601 lines
20 KiB
JavaScript
601 lines
20 KiB
JavaScript
import { MAP_H, MAP_W, hash2, indexOf, inside } from "./mapUtils.js";
|
|
|
|
export const NAME_KANJI_POOLS = {
|
|
modifiers: [
|
|
"大", "小", "上", "下", "中", "奥", "脇",
|
|
"東", "西", "南", "北",
|
|
"新", "古", "本",
|
|
"高", "長", "広", "深", "浅",
|
|
"白", "黒", "青", "赤", "藍",
|
|
"奥", "前", "後", "内", "外",
|
|
"美", "吉", "福", "幸", "徳",
|
|
"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "百", "千", "万",
|
|
"霞", "朝", "日", "天", "晴",
|
|
"土", "砂", "石", "岩",
|
|
"丑", "卯", "辰", "巳", "酉",
|
|
"早", "安", "真", "丸", "平", "勝", "吹", "舞", "鎌", "釜", "笠", "掘", "鎚", "綾", "槌",
|
|
],
|
|
|
|
inlandTerrain: [
|
|
"山", "野", "荒", "野", "沢",
|
|
"森", "林", "岡", "丘", "坂",
|
|
"峰", "峠", "嶺", "尾", "平", "坪", "延", "燧",
|
|
"窪", "久", "迫", "久保", "玖保", "漥", "佐古", "作古",
|
|
"塚", "牧", "畑", "田", "森", "幡多", "幡", "畠", "秦", "植", "生",
|
|
"聡", "郷", "里",
|
|
"馬", "鹿", "亀", "鷲", "鷹", "鶴", "竜", "龍", "牛", "鳥",
|
|
"湯",
|
|
],
|
|
|
|
waterTerrain: [
|
|
"川", "河", "江", "瀬", "淵", "渕",
|
|
"池", "沼", "泉", "井",
|
|
"滝", "梅", "沢", "澤", "谷", "津",
|
|
"水", "清", "渡", "橋", "堀",
|
|
"溝", "浦"
|
|
],
|
|
|
|
coastalTerrain: [
|
|
"津", "浦", "津", "崎",
|
|
"島", "磯", "潟", "湊", "津",
|
|
"州", "洲", "瀬", "砂", "潮", "塩", "浜",
|
|
"泊", "江", "浦", "灘", "入",
|
|
"戸", "門",
|
|
"鯵", "鰐", "漁", "魚", "鮫", "鮎",
|
|
],
|
|
|
|
plants: [
|
|
"松", "杉", "桜", "梅", "栗",
|
|
"竹", "楠", "藤", "萩", "葦",
|
|
"菅", "榎", "椿", "桐", "柳",
|
|
"橘", "柏", "槙", "柿", "桃",
|
|
"梨", "桑", "麻", "芦", "茅",
|
|
"粟", "稲", "稗", "米", "飯", "糠",
|
|
"榊", "楢", "檜", "椎", "柚", "茨", "葛", "蘆", "菖", "蒲", "蓮", "桑"
|
|
],
|
|
|
|
postfixes: [
|
|
"田", "川", "山", "岡", "森", "林",
|
|
"島",
|
|
"江", "瀬", "井", "戸", "口",
|
|
"辺", "里", "郷", "村", "町",
|
|
"宿", "庄", "台", "坂", "橋",
|
|
"本", "内", "窪", "平", "塚",
|
|
"畑", "牧", "前", "見", "中", "羽", "生", "塚", "部",
|
|
],
|
|
|
|
archaicPrefixes: [
|
|
"阿", "吾", "安", "有", "衣", "伊", "以", "井", "宇", "羽", "江", "恵", "尾", "小", "於",
|
|
"可", "加", "賀", "香", "鹿", "賀", "嘉", "喜", "紀", "吉", "伎", "久", "玖", "気", "家", "祁", "古", "子", "巨", "己",
|
|
"佐", "紗", "左", "志", "師", "須", "瀬", "曽", "蘇",
|
|
"多", "太", "知", "津", "土",
|
|
"那", "奈", "名", "仁", "尼", "根", "乃", "能",
|
|
"波", "氷", "比", "肥", "布", "夫", "戸", "保", "穂",
|
|
"間", "磨", "摩", "見", "牟", "武", "目", "女", "毛", "裳",
|
|
"弥", "矢", "夜", "耶", "由", "与",
|
|
"和", "輪",
|
|
"出", "播", "但",
|
|
"因", "伯", "筑", "肥", "豊",
|
|
"日", "紀", "志", "尾", "駿",
|
|
"甲", "信", "越", "備", "能",
|
|
"薩", "隠", "美", "三", "若",
|
|
"遠", "近", "能", "加", "賀", "度",
|
|
"越", "淡", "壱", "衣", "古", "彦", "多", "志", "布", "治"
|
|
],
|
|
|
|
archaicSuffixes: [
|
|
"井", "羽", "江", "恵", "尾", "於",
|
|
"賀", "鹿", "喜", "吉", "伎", "久", "玖", "気", "家", "祁", "古", "子",
|
|
"佐", "紗", "左", "志", "路", "師", "須", "瀬", "曽", "蘇",
|
|
"多", "太", "知", "津", "豆", "土",
|
|
"那", "奈", "名", "仁", "尼", "根", "乃", "能",
|
|
"波", "布", "夫", "戸", "保", "穂",
|
|
"間", "磨", "摩", "馬", "見", "牟", "武", "目", "女", "毛", "裳", "茂",
|
|
"弥", "矢", "夜", "耶", "由", "与", "予",
|
|
"良", "利", "礼", "呂",
|
|
"和", "輪",
|
|
"陀", "芸", "雲",
|
|
"幡", "耆", "摩",
|
|
"張", "江", "河", "斐", "濃",
|
|
"岐", "門", "隅", "向",
|
|
"居", "前", "中", "後", "波",
|
|
"勢", "渡", "城", "紫", "野", "度",
|
|
"津", "島", "信", "登", "賀", "志",
|
|
"良", "美", "智", "茂", "代", "古", "麻", "彦", "比古", "子"
|
|
],
|
|
|
|
settlementWords: [
|
|
"里", "郷", "村", "町", "宿", "垣", "坪", "軒",
|
|
"庄", "ノ庄", "之庄", "院", "宮", "ノ宮", "之宮", "寺", "社", "堂",
|
|
"城", "館", "屋", "家", "所",
|
|
"市", "場", "関", "地蔵", "辻", "角", "堰",
|
|
"ヶ沢", "ヶ谷", "ヶ浜", "ヶ崎", "ヶ島", "ヶ浦", "ヶ津", "ヶ丘",
|
|
]
|
|
};
|
|
|
|
export const NAME_PROBABILITIES = {
|
|
customNameList: 0.25,
|
|
retryCount: 24,
|
|
|
|
categoryFallback: {
|
|
modifiers: 1.0,
|
|
inlandTerrain: 1.0,
|
|
waterTerrain: 1.0,
|
|
coastalTerrain: 1.0,
|
|
plants: 1.0,
|
|
postfixes: 1.0,
|
|
archaicPrefixes: 1.0,
|
|
archaicSuffixes: 1.0,
|
|
settlementWords: 1.0,
|
|
},
|
|
|
|
contextCategoryWeights: {
|
|
generic: {
|
|
inlandTerrain: 1.0,
|
|
waterTerrain: 0.6,
|
|
coastalTerrain: 0.2,
|
|
plants: 0.8,
|
|
postfixes: 0.7,
|
|
archaic: 0.25,
|
|
settlementWords: 0.6,
|
|
},
|
|
coastal: {
|
|
inlandTerrain: 0.25,
|
|
waterTerrain: 0.8,
|
|
coastalTerrain: 1.0,
|
|
plants: 0.5,
|
|
postfixes: 0.7,
|
|
archaic: 0.2,
|
|
settlementWords: 0.6,
|
|
},
|
|
river: {
|
|
inlandTerrain: 0.5,
|
|
waterTerrain: 1.0,
|
|
coastalTerrain: 0.1,
|
|
plants: 0.6,
|
|
postfixes: 0.7,
|
|
archaic: 0.25,
|
|
settlementWords: 0.6,
|
|
},
|
|
plain: {
|
|
inlandTerrain: 1.0,
|
|
waterTerrain: 0.35,
|
|
coastalTerrain: 0.05,
|
|
plants: 0.9,
|
|
postfixes: 0.8,
|
|
archaic: 0.25,
|
|
settlementWords: 0.7,
|
|
},
|
|
mountain: {
|
|
inlandTerrain: 1.0,
|
|
waterTerrain: 0.45,
|
|
coastalTerrain: 0.02,
|
|
plants: 0.85,
|
|
postfixes: 0.55,
|
|
archaic: 0.35,
|
|
settlementWords: 0.45,
|
|
},
|
|
historic: {
|
|
inlandTerrain: 0.65,
|
|
waterTerrain: 0.45,
|
|
coastalTerrain: 0.25,
|
|
plants: 0.6,
|
|
postfixes: 0.9,
|
|
archaic: 0.75,
|
|
settlementWords: 0.9,
|
|
},
|
|
suburban: {
|
|
inlandTerrain: 0.85,
|
|
waterTerrain: 0.25,
|
|
coastalTerrain: 0.15,
|
|
plants: 0.55,
|
|
postfixes: 0.85,
|
|
archaic: 0.10,
|
|
settlementWords: 1.0,
|
|
},
|
|
},
|
|
};
|
|
|
|
export const NAME_TEMPLATES = {
|
|
modifierTerrain: {
|
|
slots: ["modifier", "terrain"],
|
|
baseWeight: 1.0,
|
|
},
|
|
plantTerrain: {
|
|
slots: ["plant", "terrain"],
|
|
baseWeight: 0.85,
|
|
},
|
|
terrainPostfix: {
|
|
slots: ["terrain", "postfix"],
|
|
baseWeight: 0.75,
|
|
},
|
|
archaicPair: {
|
|
slots: ["archaicPrefix", "archaicSuffix"],
|
|
baseWeight: 0.35,
|
|
},
|
|
archaicTerrain: {
|
|
slots: ["archaicPrefix", "terrain"],
|
|
baseWeight: 0.30,
|
|
},
|
|
modifierSettlement: {
|
|
slots: ["modifier", "settlement"],
|
|
baseWeight: 0.65,
|
|
},
|
|
};
|
|
|
|
export const NAME_TEMPLATE_WEIGHTS = {
|
|
generic: {
|
|
modifierTerrain: 1.0,
|
|
plantTerrain: 0.8,
|
|
terrainPostfix: 0.7,
|
|
archaicPair: 0.25,
|
|
archaicTerrain: 0.25,
|
|
modifierSettlement: 0.55,
|
|
},
|
|
coastal: {
|
|
modifierTerrain: 0.8,
|
|
plantTerrain: 0.55,
|
|
terrainPostfix: 0.9,
|
|
archaicPair: 0.2,
|
|
archaicTerrain: 0.25,
|
|
modifierSettlement: 0.55,
|
|
},
|
|
river: {
|
|
modifierTerrain: 0.9,
|
|
plantTerrain: 0.85,
|
|
terrainPostfix: 0.85,
|
|
archaicPair: 0.25,
|
|
archaicTerrain: 0.25,
|
|
modifierSettlement: 0.55,
|
|
},
|
|
plain: {
|
|
modifierTerrain: 1.0,
|
|
plantTerrain: 0.95,
|
|
terrainPostfix: 0.8,
|
|
archaicPair: 0.25,
|
|
archaicTerrain: 0.25,
|
|
modifierSettlement: 0.7,
|
|
},
|
|
mountain: {
|
|
modifierTerrain: 1.0,
|
|
plantTerrain: 0.9,
|
|
terrainPostfix: 0.5,
|
|
archaicPair: 0.35,
|
|
archaicTerrain: 0.45,
|
|
modifierSettlement: 0.35,
|
|
},
|
|
historic: {
|
|
modifierTerrain: 0.6,
|
|
plantTerrain: 0.45,
|
|
terrainPostfix: 0.75,
|
|
archaicPair: 0.8,
|
|
archaicTerrain: 0.55,
|
|
modifierSettlement: 0.95,
|
|
},
|
|
suburban: {
|
|
modifierTerrain: 0.85,
|
|
plantTerrain: 0.45,
|
|
terrainPostfix: 0.65,
|
|
archaicPair: 0.1,
|
|
archaicTerrain: 0.1,
|
|
modifierSettlement: 1.0,
|
|
},
|
|
};
|
|
|
|
// Add preferred reusable place names here. Each generated entity has a
|
|
// deterministic chance to use one before falling back to template kanji.
|
|
export const CUSTOM_NAME_LIST = [];
|
|
|
|
// Legacy export kept only so older imports do not fail.
|
|
export const NAME_PARTS = {};
|
|
|
|
const POOL_KEYS = Object.keys(NAME_KANJI_POOLS);
|
|
const TERRAIN_POOL_KEYS = ["inlandTerrain", "waterTerrain", "coastalTerrain"];
|
|
const ASCII_DIAGNOSTIC_PREFIX = "N";
|
|
|
|
function stableHash(value) {
|
|
const text = String(value || "");
|
|
let h = 2166136261;
|
|
for (let i = 0; i < text.length; i++) {
|
|
h ^= text.charCodeAt(i);
|
|
h = Math.imul(h, 16777619) >>> 0;
|
|
}
|
|
return h >>> 0;
|
|
}
|
|
|
|
function roll(seed, id, attempt, salt) {
|
|
const h = stableHash(id);
|
|
return hash2(h + attempt * 1009 + salt * 9173, seed + salt * 1013, (h ^ seed ^ salt) >>> 0);
|
|
}
|
|
|
|
function pick(pool, seed, id, attempt, salt) {
|
|
if (!pool?.length) return null;
|
|
const index = Math.floor(roll(seed, id, attempt, salt) * pool.length) % pool.length;
|
|
return pool[index] || null;
|
|
}
|
|
|
|
function countChars(value) {
|
|
return Array.from(String(value || "")).length;
|
|
}
|
|
|
|
function isKanjiChar(ch) {
|
|
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(ch);
|
|
}
|
|
|
|
function hasRepeatedKanji(value) {
|
|
const seen = new Set();
|
|
for (const ch of Array.from(String(value || ""))) {
|
|
if (!isKanjiChar(ch)) continue;
|
|
if (seen.has(ch)) return true;
|
|
seen.add(ch);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function incrementCounter(counter, key, amount = 1) {
|
|
counter[key] = (counter[key] || 0) + amount;
|
|
}
|
|
|
|
function boundedIndex(entity) {
|
|
const x = Math.max(0, Math.min(MAP_W - 1, Math.round(entity?.x || 0)));
|
|
const y = Math.max(0, Math.min(MAP_H - 1, Math.round(entity?.y || 0)));
|
|
return indexOf(x, y);
|
|
}
|
|
|
|
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 contextWeightsFor(context, probabilities = NAME_PROBABILITIES) {
|
|
return probabilities.contextCategoryWeights?.[context] || probabilities.contextCategoryWeights?.generic || {};
|
|
}
|
|
|
|
function poolWeight(poolKey, context, probabilities = NAME_PROBABILITIES) {
|
|
const weights = contextWeightsFor(context, probabilities);
|
|
const fallback = probabilities.categoryFallback?.[poolKey] ?? 1.0;
|
|
if (poolKey === "archaicPrefixes" || poolKey === "archaicSuffixes") return (weights.archaic ?? 1.0) * fallback;
|
|
return (weights[poolKey] ?? 1.0) * fallback;
|
|
}
|
|
|
|
function slotPoolKeys(slot, context, probabilities = NAME_PROBABILITIES) {
|
|
if (slot === "terrain") {
|
|
return weightedKeyOrder(TERRAIN_POOL_KEYS, context, probabilities, 113);
|
|
}
|
|
const map = {
|
|
modifier: "modifiers",
|
|
plant: "plants",
|
|
postfix: "postfixes",
|
|
archaicPrefix: "archaicPrefixes",
|
|
archaicSuffix: "archaicSuffixes",
|
|
settlement: "settlementWords",
|
|
};
|
|
return map[slot] ? [map[slot]] : [];
|
|
}
|
|
|
|
function weightedKeyOrder(keys, context, probabilities, salt) {
|
|
return [...keys].sort((a, b) => {
|
|
const wa = poolWeight(a, context, probabilities);
|
|
const wb = poolWeight(b, context, probabilities);
|
|
if (wb !== wa) return wb - wa;
|
|
return stableHash(`${a}:${salt}`) - stableHash(`${b}:${salt}`);
|
|
});
|
|
}
|
|
|
|
function weightedChoice(entries, seed, id, attempt, salt) {
|
|
const active = entries.filter((entry) => entry.weight > 0);
|
|
const total = active.reduce((sum, entry) => sum + entry.weight, 0);
|
|
if (total <= 0) return null;
|
|
let target = roll(seed, id, attempt, salt) * total;
|
|
for (const entry of active) {
|
|
target -= entry.weight;
|
|
if (target <= 0) return entry.key;
|
|
}
|
|
return active[active.length - 1]?.key || null;
|
|
}
|
|
|
|
function pickWeightedPoolKey(keys, context, pools, probabilities, seed, id, attempt, salt) {
|
|
const entries = keys.map((key) => ({
|
|
key,
|
|
weight: poolWeight(key, context, probabilities) * (pools[key]?.length ? 1 : 0),
|
|
}));
|
|
return weightedChoice(entries, seed, id, attempt, salt);
|
|
}
|
|
|
|
export function createNameDebug(probabilities = NAME_PROBABILITIES, pools = NAME_KANJI_POOLS) {
|
|
const emptyPools = POOL_KEYS.filter((key) => !pools[key]?.length);
|
|
const poolsPresent = Object.fromEntries(POOL_KEYS.map((key) => [key, Boolean(pools[key]?.length)]));
|
|
return {
|
|
effectiveCustomNameListProbability: probabilities.customNameList,
|
|
poolsPresent,
|
|
emptyPools,
|
|
selectedTemplateCounts: {},
|
|
selectedContextCounts: {},
|
|
generatedNamesUsed: 0,
|
|
customNameListUsed: 0,
|
|
invalidNamesRejected: 0,
|
|
repeatedKanjiNamesRejected: 0,
|
|
oneCharacterNamesPrevented: 0,
|
|
rejectedOneCharacterNames: 0,
|
|
duplicateRetries: 0,
|
|
fallbackAttempts: 0,
|
|
legacyFallbackUsed: 0,
|
|
oneKanjiAppendFallbackUsed: 0,
|
|
derivedNameCount: 0,
|
|
maxDerivedPerBase: 0,
|
|
};
|
|
}
|
|
|
|
export 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 || 0, entity?.y || 0, fields?.sea);
|
|
const river = (fields?.river?.[i] || 0) > 0.35 || (fields?.flowAccum?.[i] || 0) > 0.55 || (fields?.valleyField?.[i] || 0) > 0.42;
|
|
const plain = (fields?.plain?.[i] || 0) > 0.48 || (fields?.agriculture?.[i] || 0) > 0.46;
|
|
const mountain = (fields?.elevation?.[i] || 0) > 0.52 || (fields?.slope?.[i] || 0) > 0.35 || (fields?.ridgeField?.[i] || 0) > 0.42;
|
|
const urban = (fields?.populationDensity?.[i] || 0) > 0.35 || [2, 3, 4, 7, 8].includes(fields?.landuse?.[i]);
|
|
|
|
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("newtown") || kind.includes("new town") || kind.includes("satellite") || kind.includes("station") || urban) return "suburban";
|
|
if (coastal && !mountain) return "coastal";
|
|
if (river) return "river";
|
|
if (mountain && !plain) return "mountain";
|
|
if (plain) return "plain";
|
|
return "generic";
|
|
}
|
|
|
|
export function resolveSlotPool(slot, context, pools = NAME_KANJI_POOLS, probabilities = NAME_PROBABILITIES, seed = 0, id = "", attempt = 0) {
|
|
const keys = slotPoolKeys(slot, context, probabilities);
|
|
if (!keys.length) return null;
|
|
const selectedKey = pickWeightedPoolKey(keys, context, pools, probabilities, seed, id, attempt, 1501 + stableHash(slot));
|
|
if (selectedKey) return { key: selectedKey, pool: pools[selectedKey] };
|
|
for (const key of weightedKeyOrder(keys, context, probabilities, 1559)) {
|
|
if (pools[key]?.length) return { key, pool: pools[key] };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function validateGeneratedName(name, options = {}) {
|
|
const value = String(name || "");
|
|
const length = countChars(value);
|
|
if (!value) return { valid: false, reason: "empty" };
|
|
if (value.includes("\uFFFD")) return { valid: false, reason: "replacement" };
|
|
if (!options.allowAsciiDiagnostic && value.startsWith(ASCII_DIAGNOSTIC_PREFIX) && /^N[0-9A-Z]+$/.test(value)) {
|
|
return { valid: false, reason: "asciiDiagnostic" };
|
|
}
|
|
if (!options.allowOneCharacter && length < 2) return { valid: false, reason: "oneCharacter" };
|
|
if (!options.allowLong && length > 4) return { valid: false, reason: "tooLong" };
|
|
if (!options.allowRepeatedKanji && hasRepeatedKanji(value)) return { valid: false, reason: "repeatedKanji" };
|
|
return { valid: true, reason: "valid" };
|
|
}
|
|
|
|
function chooseTemplate(context, seed, id, attempt) {
|
|
const contextWeights = NAME_TEMPLATE_WEIGHTS[context] || NAME_TEMPLATE_WEIGHTS.generic;
|
|
const entries = Object.entries(NAME_TEMPLATES).map(([key, template]) => ({
|
|
key,
|
|
weight: (template.baseWeight ?? 1.0) * (contextWeights?.[key] ?? 0),
|
|
}));
|
|
return weightedChoice(entries, seed, id, attempt, 2003);
|
|
}
|
|
|
|
function generateTemplateNameDetails(seed, id, entity, fields, attempt, usedNames, pools = NAME_KANJI_POOLS, probabilities = NAME_PROBABILITIES) {
|
|
const context = chooseNameContext(entity, fields);
|
|
const templateKey = chooseTemplate(context, seed, id, attempt);
|
|
const template = NAME_TEMPLATES[templateKey];
|
|
if (!template) return { name: null, context, templateKey: null };
|
|
|
|
const parts = [];
|
|
for (let slotIndex = 0; slotIndex < template.slots.length; slotIndex++) {
|
|
const slot = template.slots[slotIndex];
|
|
const slotPool = resolveSlotPool(slot, context, pools, probabilities, seed, id, attempt + slotIndex);
|
|
if (!slotPool?.pool?.length) return { name: null, context, templateKey };
|
|
const part = pick(slotPool.pool, seed, id, attempt, 2503 + slotIndex * 127 + stableHash(slotPool.key));
|
|
if (!part) return { name: null, context, templateKey };
|
|
parts.push(part);
|
|
}
|
|
|
|
const name = parts.join("");
|
|
const validation = validateGeneratedName(name);
|
|
if (!validation.valid) return { name: null, context, templateKey, invalidReason: validation.reason };
|
|
if (usedNames?.has(name)) return { name: null, context, templateKey, duplicate: true };
|
|
return { name, context, templateKey };
|
|
}
|
|
|
|
export function generateTemplateName(seed, id, entity, fields, attempt = 0, usedNames = null) {
|
|
return generateTemplateNameDetails(seed, id, entity, fields, attempt, usedNames).name;
|
|
}
|
|
|
|
function deterministicDiagnosticName(seed, id, attempt = 0) {
|
|
const h = Math.floor(roll(seed, id, attempt, 3001) * 0xb640).toString(36).toUpperCase();
|
|
return `${ASCII_DIAGNOSTIC_PREFIX}${h.padStart(3, "0")}`;
|
|
}
|
|
|
|
function uniqueDiagnosticName(seed, id, usedNames, debug, startAttempt = 0) {
|
|
for (let offset = 0; offset < 128; offset++) {
|
|
const name = deterministicDiagnosticName(seed, id, startAttempt + offset);
|
|
if (!usedNames?.has(name)) return name;
|
|
debug.duplicateRetries++;
|
|
}
|
|
return `${ASCII_DIAGNOSTIC_PREFIX}${stableHash(`${seed}:${id}`).toString(36).toUpperCase()}`;
|
|
}
|
|
|
|
function tryCustomNameList(seed, id, usedNames, debug) {
|
|
if (!CUSTOM_NAME_LIST.length) return null;
|
|
if (roll(seed, id, 0, 3527) >= (NAME_PROBABILITIES.customNameList ?? 0)) return null;
|
|
|
|
const start = Math.floor(roll(seed, id, 0, 3539) * CUSTOM_NAME_LIST.length) % CUSTOM_NAME_LIST.length;
|
|
for (let offset = 0; offset < CUSTOM_NAME_LIST.length; offset++) {
|
|
const customName = CUSTOM_NAME_LIST[(start + offset) % CUSTOM_NAME_LIST.length];
|
|
const validation = validateGeneratedName(customName, { allowAsciiDiagnostic: true });
|
|
if (!validation.valid) {
|
|
if (validation.reason === "oneCharacter") {
|
|
debug.oneCharacterNamesPrevented++;
|
|
debug.rejectedOneCharacterNames++;
|
|
} else if (validation.reason === "repeatedKanji") {
|
|
debug.repeatedKanjiNamesRejected++;
|
|
} else {
|
|
debug.invalidNamesRejected++;
|
|
}
|
|
continue;
|
|
}
|
|
if (usedNames?.has(customName)) {
|
|
debug.duplicateRetries++;
|
|
continue;
|
|
}
|
|
debug.customNameListUsed++;
|
|
return customName;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function generateEntityName(seed, id, entity, fields, usedNames = null, debug = createNameDebug()) {
|
|
debug ||= createNameDebug();
|
|
const listedCustomName = tryCustomNameList(seed, id, usedNames, debug);
|
|
if (listedCustomName) return listedCustomName;
|
|
|
|
const retryCount = Math.max(1, NAME_PROBABILITIES.retryCount || 1);
|
|
for (let attempt = 0; attempt < retryCount; attempt++) {
|
|
const result = generateTemplateNameDetails(seed, id, entity, fields, attempt, usedNames);
|
|
if (result.context) incrementCounter(debug.selectedContextCounts, result.context);
|
|
if (result.templateKey) incrementCounter(debug.selectedTemplateCounts, result.templateKey);
|
|
if (result.duplicate) {
|
|
debug.duplicateRetries++;
|
|
continue;
|
|
}
|
|
if (result.invalidReason) {
|
|
if (result.invalidReason === "oneCharacter") {
|
|
debug.oneCharacterNamesPrevented++;
|
|
debug.rejectedOneCharacterNames++;
|
|
}
|
|
else if (result.invalidReason === "repeatedKanji") debug.repeatedKanjiNamesRejected++;
|
|
else debug.invalidNamesRejected++;
|
|
continue;
|
|
}
|
|
if (result.name) {
|
|
debug.generatedNamesUsed++;
|
|
return result.name;
|
|
}
|
|
}
|
|
|
|
debug.fallbackAttempts++;
|
|
return uniqueDiagnosticName(seed, id, usedNames, debug, retryCount);
|
|
}
|
|
|
|
export function contextualDefaultName(seed, id, entity, nameParts = NAME_PARTS, fields, attempt = 0) {
|
|
const name = generateTemplateName(seed, id, entity, fields, attempt, null);
|
|
return name || deterministicDiagnosticName(seed, id, attempt);
|
|
}
|
|
|
|
export function chooseNameSuffixPool() {
|
|
return NAME_KANJI_POOLS.postfixes;
|
|
}
|