245 lines
11 KiB
JavaScript
245 lines
11 KiB
JavaScript
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: [
|
|
"前", "内", "母", "父",
|
|
"川", "山", "森", "松", "原", "黍", "栃", "梨", "柏", "桜", "杉", "柳", "葛", "竹", "梅", "藤", "篠", "米", "馬", "石", "岩", "鷲", "鳥", "鷹", "犬", "猿", "鹿", "熊", "羽", "木", "林", "根", "葉", "葦", "岐","日", "月", "島", "畠", "野", "田",
|
|
"井", "守", "盛", "幡", "河内", "敷", "鍋", "凪", "太", "嘉", "倉", "昌", "芸", "相", "生", "折", "摂", "香", "佳", "加", "笠", "行", "油", "柚", "関", "伯", "桶", "久", "代", "旭", "手", "蓑", "仙", "豊", "飾", "戸", "矢", "玉", "須", "津", "佐", "志", "布", "蘇", "曾", "庄", "甲", "尾", "朝", "比古", "目", "賀", "彦", "姫", "輪", "和", "福", "岡", "富", "住", "由", "君", "飯", "那", "奈", "肥", "越", "曲輪", "車", "渋", "幸",
|
|
],
|
|
|
|
// 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: ["館", "宮", "寺", "社", "陣", "府", "町"],
|
|
},
|
|
|
|
// Context-aware morphemes that may appear only at the end of a name.
|
|
contextSuffixes: {
|
|
coastal: ["浜", "湊", "灘", "浦", "津", "崎"],
|
|
river: ["川", "瀬", "橋", "渡", "沼", "沢"],
|
|
plain: ["原", "野", "田", "里", "村", "平"],
|
|
mountain: ["谷", "峡", "峰", "山", "沢", "岳"],
|
|
historic: ["城", "通", "館", "宮", "町"],
|
|
},
|
|
};
|
|
|
|
export const NAME_PROBABILITIES = {
|
|
prefixOnly: 0.36,
|
|
contextFreeAsMain: 0.34,
|
|
additionalContextFree: 0.22,
|
|
secondFree: 0.18,
|
|
finalContextSuffix: 0.10,
|
|
finalSuffixOnly: 0.10,
|
|
finalFree: 0.20,
|
|
twoCharacter: 0.90,
|
|
customName: 0.20,
|
|
};
|
|
|
|
export const CONTEXT_SUFFIXES = NAME_PARTS.contextSuffixes;
|
|
|
|
export const CUSTOM_NAMES = {
|
|
// "city-0": "Aohara",
|
|
// "port-0": "Shirahama",
|
|
// "castle-0": "Kurono",
|
|
"city-0": "加茂",
|
|
"city-1": "高砂",
|
|
"city-2": "八幡",
|
|
"city-3": "国府",
|
|
"city-4": "春日",
|
|
"city-5": "御幸",
|
|
"city-6": "住吉",
|
|
"city-7": "日向",
|
|
"city-8": "加茂",
|
|
"city-9": "加茂",
|
|
"city-10": "飛鳥",
|
|
"city-11": "諏訪",
|
|
"city-12": "相模",
|
|
"city-13": "鹿島",
|
|
"city-14": "香取",
|
|
"city-15": "住吉",
|
|
"city-16": "国文",
|
|
"city-17": "愛宕",
|
|
"city-18": "安曇",
|
|
"castle-0": "佐古",
|
|
};
|
|
|
|
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) || "原";
|
|
}
|