tarinai/js/tarinai_seed_factory.js
2026-07-10 16:40:06 +09:00

158 lines
6.2 KiB
JavaScript

"use strict";
(function (global) {
function hashString(value = "") {
const str = String(value || "");
let h = 2166136261;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return (h >>> 0).toString(36);
}
function worldSeedFromWords(high = 0, low = 0) {
return `w${(Number(high) >>> 0).toString(16).padStart(8, "0")}${(Number(low) >>> 0).toString(16).padStart(8, "0")}`;
}
function createWorldSeed() {
const cryptoObj = global.crypto;
if (cryptoObj?.getRandomValues) {
const buf = new Uint32Array(2);
cryptoObj.getRandomValues(buf);
return worldSeedFromWords(buf[0], buf[1]);
}
const high = Math.floor(Date.now() / 0x100000000) ^ Math.floor(Math.random() * 0xffffffff);
const low = (Date.now() >>> 0) ^ Math.floor(Math.random() * 0xffffffff);
return worldSeedFromWords(high, low);
}
function ensureWorldSeed(worldRef = null) {
if (!worldRef) return createWorldSeed();
if (!worldRef.worldSeed) worldRef.worldSeed = createWorldSeed();
if (!Number.isFinite(worldRef.birthSerial)) worldRef.birthSerial = 0;
return worldRef.worldSeed;
}
function childSeed(worldSeed = "", serial = 0, parentSeeds = []) {
const safeSerial = Math.max(0, Math.floor(Number(serial) || 0));
const parents = (parentSeeds || []).filter(Boolean).join("+");
// \u533a\u5207\u308a\u3092\u5165\u308c\u3066\u304a\u304f\u3068\u3001\u5c06\u6765 birthSeed \u81ea\u4f53\u3092\u4fdd\u5b58\u3057\u306a\u3044\u5f62\u5f0f\u3078\u79fb\u308b\u6642\u306b
// birthSerial \u3092\u5b89\u5168\u306b\u53d6\u308a\u51fa\u305b\u308b\u3002
return `b${safeSerial.toString(36)}_${hashString(`${worldSeed}|${safeSerial}|${parents}`)}`;
}
function serialFromBirthSeed(seed = "") {
const match = /^b([0-9a-z]+)_/i.exec(String(seed || ""));
if (!match) return 0;
const n = parseInt(match[1], 36);
return Number.isFinite(n) && n > 0 ? n : 0;
}
function prepareAddOptions(worldRef, opts = {}) {
const next = { ...(opts || {}) };
ensureWorldSeed(worldRef);
if (!next.birthSeed) {
worldRef.birthSerial = Math.max(0, Number(worldRef.birthSerial) || 0) + 1;
next.birthSerial = worldRef.birthSerial;
// Save schema 33 derives identity from worldSeed + birthSerial. Parentage is
// stored independently, so it must not be duplicated inside birthSeed.
next.birthSeed = childSeed(worldRef.worldSeed, next.birthSerial, []);
} else {
if (!Number.isFinite(next.birthSerial)) {
const parsedSerial = serialFromBirthSeed(next.birthSeed);
if (parsedSerial > 0) next.birthSerial = parsedSerial;
}
if (Number.isFinite(next.birthSerial)) {
worldRef.birthSerial = Math.max(Number(worldRef.birthSerial) || 0, Number(next.birthSerial) || 0);
}
}
if (!next.familyKey) next.familyKey = next.birthSeed;
return next;
}
function seedUnit(seed = "", key = "") {
return stableUnit(seed, key);
}
function seedPick(seed = "", key = "", list = []) {
if (!list?.length) return null;
const i = Math.min(list.length - 1, Math.floor(seedUnit(seed, key) * list.length));
return list[i];
}
function seededName(seed = "") {
const len = 2 + Math.floor(seedUnit(seed, "name.len") * 6); // 2..7
const hasStop = len >= 2 && seedUnit(seed, "name.stop") < 0.30;
const middleLen = Math.max(0, len - 1 - (hasStop ? 1 : 0));
let name = "\u306f";
for (let i = 0; i < middleLen; i++) name += seedUnit(seed, `name.u.${i}`) < 0.64 ? "\u3046" : "\u3045";
if (hasStop) name += "\u3063";
return name;
}
function seededType(seed = "") {
const list = typeof spawnableSprites === "function" ? spawnableSprites().map(s => s.id).filter(Boolean) : ["smile"];
return seedPick(seed, "sprite.base", list) || "smile";
}
function seededGoodMode(seed = "") {
const list = typeof displayNormalSprites === "function" ? displayNormalSprites() : ["smile", "jito", "normal_smirk"];
return seedPick(seed, "sprite.good", list) || "smile";
}
function seededPersonality(seed = "") {
if (typeof randomBirthPersonality === "function") return randomBirthPersonality(seed);
return { aggression: 0, openness: 0, sociability: 0, neuroticism: 0 };
}
function seededGenetics(seed = "") {
if (typeof normalizeGenetics === "function") return normalizeGenetics(null, seed);
return { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1, temperatureOffset: 0 };
}
function seededAdultScale(seed = "", genetics = null) {
if (typeof adultScaleFromGenetics === "function") return adultScaleFromGenetics(seed, genetics);
return 0.28;
}
function seededLifeSpan(seed = "", genetics = null) {
const g = genetics || seededGenetics(seed);
const base = 1400 + seedUnit(seed, "life.span") * 800;
let span = base * (g.lifeSpanMul || 1);
if (span < 900) span = (1200 + seedUnit(seed, "life.span.low") * 600) * (g.lifeSpanMul || 1);
return span;
}
function birthProfile(seed = "", opts = {}) {
const zunchiSlave = Boolean(opts.isZunchiSlave || opts.zunchiSlaveLocked);
const genetics = opts.genetics && typeof normalizeGenetics === "function" ? normalizeGenetics(opts.genetics, seed) : seededGenetics(seed);
const personality = zunchiSlave && typeof zunchiSlavePersonalityValue === "function" ? zunchiSlavePersonalityValue() : seededPersonality(seed);
return {
birthSeed: seed,
name: zunchiSlave ? "\u305a\u3093\u3061\u3069\u308c\u3044" : seededName(seed),
type: zunchiSlave ? "zunchi_slave" : seededType(seed),
goodMode: zunchiSlave ? "zunchi_slave" : seededGoodMode(seed),
birthPersonality: personality,
genetics,
adultScale: seededAdultScale(seed, genetics),
lifeSpan: seededLifeSpan(seed, genetics),
};
}
function personalityArray(seed = "") {
const p = birthProfile(seed).birthPersonality || {};
return ["aggression", "openness", "sociability", "neuroticism"].map(k => Math.round(((Number(p[k]) || 0) + 1) * 100));
}
global.TarinaiSeedFactory = Object.freeze({
createWorldSeed,
worldSeedFromWords,
ensureWorldSeed,
childSeed,
serialFromBirthSeed,
prepareAddOptions,
birthProfile,
personalityArray,
});
})(typeof window !== "undefined" ? window : globalThis);