import { MAP_H, MAP_W, indexOf, inside } from "./grid.js"; import { hash2 } from "./random.js"; // Default and custom place-name resources. // Edit only this file to change generated names or override individual IDs. export const NAME_PARTS = { // May appear only at the beginning of a name. prefixOnly: ["大", "小", "新", "古", "浅", "深", "上", "下", "東", "西", "南", "北", "中", "本", "広", "元", "奥", "前", "高", "長", "丸", "一", "壱", "二", "三", "四", "五", "六", "七", "八", "九", "十", "五十", "百", "千", "万", "阿", "佐々", "代々", "千代", "宇", "伊", "牟", "美",], // Generic morphemes that may appear at the beginning or in the middle. free: [ //pattern "青", "白", "黒", "赤", "藍", //nature "川", "山", "森", "松", "竹", "梅", "柳", "栃", "橘", "柏", "樫", "麻", "桜", "岡", "木", "林", "森", "根", "葉", "葦", "日", "月", "君", "野", "尾", "旭", "朝", "霧", //objects "畠", "秦", "幡", "井", "真", "田", "井", "口", "関", "矢", "手", "戸", "庄", "屋", "土", "鋸", "琴", "笙", "彦", "比", "碑", "多", "", "", //others "由", "志", "成", "幸", "徳", "甲", "由", "和", "清", "弘", "保", "徳", "福", "富", "住", "幸", "弥", "住", "生", "昌", "相", "香", "吉", "𠮷", "佐", "多", "保", "加" ], // May appear only at the end of a name. suffixOnly: ["宿", "口", "摩", "磨", "潟", "茂"], // Context-aware morphemes that may appear at the beginning or in the middle. contextFree: { coastal: ["浜", "浦", "津", "湊", "潮", "磯", "海", "島", "磯", "渚", "潮", "汐"], river: ["瀬", "川", "橋", "渡", "沢", "淵", "沼", "堀"], plain: ["原", "野", "田", "畠", "里", "平", "牧", "塚"], mountain: ["山", "谷", "沢", "峰", "尾", "久保", "迫", "岳"], historic: ["城", "館", "宮", "寺", "社", "陣", "府", "町"], suburban: [], }, // Context-aware morphemes that may appear only at the end of a name. contextSuffixes: { coastal: ["浜", "湊", "灘", "浦", "津", "崎", "嶋"], river: ["川", "瀬", "橋", "渡", "沼", "沢"], plain: ["原", "野", "田", "里", "村", "平"], mountain: ["谷", "峡", "尾根", "山", "沢", "麓"], historic: ["城", "通", "館", "宮", "町"], suburban: ["丘", "台", "野", "原", "新町", "ヶ丘"], }, }; export const NAME_PROBABILITIES = { prefixOnly: 0.36, contextFreeAsMain: 0.34, additionalContextFree: 0.22, secondFree: 0.18, finalContextSuffix: 0.10, finalSuffixOnly: 0.50, finalFree: 0.20, twoCharacter: 0.80, }; export const CONTEXT_SUFFIXES = NAME_PARTS.contextSuffixes; export const CUSTOM_NAMES = { // "city-0": "Aohara", // "port-0": "Shirahama", // "castle-0": "Kurono", }; function isNearSea(x, y, sea, radius = 3) { if (!sea) return false; for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { const nx = x + dx; const ny = y + dy; if (inside(nx, ny) && sea[indexOf(nx, ny)]) return true; } } return false; } function boundedIndex(entity) { return indexOf(Math.max(0, Math.min(MAP_W - 1, entity.x)), Math.max(0, Math.min(MAP_H - 1, entity.y))); } function chooseNameContext(entity, fields) { const kind = String(entity.kind || "").toLowerCase(); const i = boundedIndex(entity); const coastal = (fields?.coastalLowland?.[i] || 0) > 0.35 || isNearSea(entity.x, entity.y, fields?.sea); const river = (fields?.river?.[i] || 0) > 0.35 || (fields?.valleyField?.[i] || 0) > 0.42; const plain = (fields?.plain?.[i] || 0) > 0.48 || (fields?.agriculture?.[i] || 0) > 0.46 || (fields?.basinField?.[i] || 0) > 0.35; const mountain = (fields?.elevation?.[i] || 0) > 0.52 || (fields?.slope?.[i] || 0) > 0.35 || (fields?.ridgeField?.[i] || 0) > 0.42; if (kind.includes("port") || kind.includes("harbor") || entity.portClass) return "coastal"; if (kind.includes("crossing") || kind.includes("bridge")) return "river"; if (kind.includes("pass") || kind.includes("mountain")) return "mountain"; if (kind.includes("castle") || kind.includes("market")) return "historic"; if (kind.includes("new town") || kind.includes("satellite")) return "suburban"; if (coastal && !mountain) return "coastal"; if (river) return "river"; if (mountain && !plain) return "mountain"; if (plain) return "plain"; return "plain"; } export function chooseNameSuffixPool(entity, fields) { const context = chooseNameContext(entity, fields); return NAME_PARTS.contextSuffixes[context] || NAME_PARTS.contextSuffixes.plain; } function chooseContextFreePool(entity, fields, nameParts) { const context = chooseNameContext(entity, fields); return nameParts.contextFree?.[context] || nameParts.contextFree?.plain || nameParts.free || [""]; } function pick(pool, seed, n, salt) { if (!pool?.length) return ""; const index = Math.floor(hash2(n + salt * 101, seed + salt * 1009, seed + n * 37 + salt) * pool.length) % pool.length; return pool[index]; } function chance(seed, n, salt, probability) { return hash2(n + salt * 173, seed + salt * 811, seed + n * 43 + salt) < probability; } function characterLength(value) { return Array.from(String(value || "")).length; } function oneCharacterPool(pool, fallbackPool = pool) { const filtered = (pool || []).filter((part) => characterLength(part) === 1); return filtered.length ? filtered : (fallbackPool || [""]); } function appendPart(parts, part) { if (!part) return; const previous = parts[parts.length - 1] || ""; if (previous && (previous === part || previous.endsWith(part) || part.startsWith(previous))) return; parts.push(part); } function appendFinalPart(parts, part, fallbackPool, seed, n, salt) { let finalPart = part; if (finalPart && parts.some((existing) => existing === finalPart || existing.endsWith(finalPart))) { finalPart = pick(fallbackPool, seed, n, salt + 1); } appendPart(parts, finalPart); } function trimNameParts(parts) { if (parts.length <= 3) return parts; return [parts[0], ...parts.slice(-2)]; } export function contextualDefaultName(seed, id, entity, nameParts = NAME_PARTS, fields, attempt = 0) { const prefixKey = id.split("-")[0]; const n = Number(id.split("-")[1] || 0) + attempt * 997; const prefixOnly = nameParts.prefixOnly || nameParts.prefixes || [""]; const free = nameParts.free || nameParts.infixes || [""]; const suffixOnly = nameParts.suffixOnly || nameParts.suffixes || [""]; const contextFree = chooseContextFreePool(entity, fields, nameParts); const contextSuffixes = chooseNameSuffixPool(entity, fields); const probabilities = nameParts.probabilities || NAME_PROBABILITIES; if (chance(seed, n, 7 + prefixKey.length, probabilities.twoCharacter ?? 0.80)) { const onePrefixOnly = oneCharacterPool(prefixOnly); const oneFree = oneCharacterPool(free); const oneSuffixOnly = oneCharacterPool(suffixOnly, oneFree); const oneContextFree = oneCharacterPool(contextFree, oneFree); const oneContextSuffixes = oneCharacterPool(contextSuffixes, oneSuffixOnly); const usePrefix = chance(seed, n, 11 + prefixKey.length, probabilities.prefixOnly); const useContextAsMain = chance(seed, n, 13 + prefixKey.length, probabilities.contextFreeAsMain ?? probabilities.contextFree ?? 0.34); const firstPart = usePrefix ? pick(onePrefixOnly, seed, n, 101) : pick(useContextAsMain ? oneContextFree : oneFree, seed, n, useContextAsMain ? 211 : 223); const finalContextProbability = probabilities.finalContextSuffix ?? 0.10; const finalSuffixProbability = probabilities.finalSuffixOnly ?? 0.50; const finalFreeProbability = probabilities.finalFree ?? Math.max(0, 1 - finalContextProbability - finalSuffixProbability); const finalTotal = Math.max(0.0001, finalContextProbability + finalSuffixProbability + finalFreeProbability); const finalRoll = hash2(n + prefixKey.length * 23, seed + n * 37, seed + 7919); const contextThreshold = finalContextProbability / finalTotal; const suffixThreshold = (finalContextProbability + finalSuffixProbability) / finalTotal; let finalPart; if (finalRoll < contextThreshold) { finalPart = pick(oneContextSuffixes, seed, n, 503); } else if (finalRoll < suffixThreshold) { finalPart = pick(oneSuffixOnly, seed, n, 509); } else { finalPart = pick(oneFree, seed, n, 521); } if (finalPart === firstPart) finalPart = pick(oneSuffixOnly, seed, n, 601); if (finalPart === firstPart) finalPart = pick(oneFree, seed, n, 607); return `${firstPart || pick(oneFree, seed, n, 613)}${finalPart || pick(oneSuffixOnly, seed, n, 617)}`; } const parts = []; if (chance(seed, n, 11 + prefixKey.length, probabilities.prefixOnly)) { appendPart(parts, pick(prefixOnly, seed, n, 101)); } const useContextAsMain = chance(seed, n, 13 + prefixKey.length, probabilities.contextFreeAsMain ?? probabilities.contextFree ?? 0.34); appendPart(parts, pick(useContextAsMain ? contextFree : free, seed, n, useContextAsMain ? 211 : 223)); if (!useContextAsMain && chance(seed, n, 17 + prefixKey.length, probabilities.additionalContextFree ?? probabilities.contextFree ?? 0.22)) { appendPart(parts, pick(contextFree, seed, n, 307)); } if (chance(seed, n, 19 + prefixKey.length, probabilities.secondFree)) { appendPart(parts, pick(free, seed, n, 401)); } const finalRoll = hash2(n + prefixKey.length * 23, seed + n * 37, seed + 7919); let finalPart; const finalContextProbability = probabilities.finalContextSuffix ?? 0.30; const finalSuffixProbability = probabilities.finalSuffixOnly ?? 0.50; const finalFreeProbability = probabilities.finalFree ?? Math.max(0, 1 - finalContextProbability - finalSuffixProbability); const finalTotal = Math.max(0.0001, finalContextProbability + finalSuffixProbability + finalFreeProbability); const contextThreshold = finalContextProbability / finalTotal; const suffixThreshold = (finalContextProbability + finalSuffixProbability) / finalTotal; if (finalRoll < contextThreshold) { finalPart = pick(contextSuffixes, seed, n, 503); } else if (finalRoll < suffixThreshold) { finalPart = pick(suffixOnly, seed, n, 509); } else { finalPart = pick(free, seed, n, 521); } appendFinalPart(parts, finalPart, suffixOnly, seed, n, 601); const name = trimNameParts(parts).join(""); return name || pick(suffixOnly, seed, n, 601) || "原"; }