init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
import {
collectCreatureArchetypeSignals,
resolveCreatureArchetypeForSource,
} from '../services/customWorldReferenceSignals';
import {
type CustomWorldNpc,
type CustomWorldPlayableNpc,
type CustomWorldProfile,
WorldType,
} from '../types';
import { resolveCustomWorldCompatibilityTemplateWorldType } from '../services/customWorldTheme';
import {
getMonsterPresetsByWorld,
type HostileNpcPreset,
} from './hostileNpcPresets';
type CustomWorldMonsterSource = Partial<
Pick<
CustomWorldNpc & CustomWorldPlayableNpc,
| 'name'
| 'title'
| 'role'
| 'description'
| 'backstory'
| 'personality'
| 'motivation'
| 'combatStyle'
| 'initialAffinity'
| 'relationshipHooks'
| 'tags'
>
>;
const MONSTER_SIGNAL_PATTERN =
/||||||||||||||||||||||||||/u;
const MONSTER_SIGNAL_STOP_CHARS = new Set([
'妖',
'魔',
'鬼',
'怪',
'兽',
'灵',
'尸',
'祟',
'凶',
'异',
'夜',
'古',
]);
function hashText(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return hash >>> 0;
}
function getMonsterPresetPool(worldType?: WorldType | null) {
if (worldType) {
return getMonsterPresetsByWorld(worldType);
}
const seen = new Set<string>();
return [
...getMonsterPresetsByWorld(WorldType.WUXIA),
...getMonsterPresetsByWorld(WorldType.XIANXIA),
].filter((preset) => {
if (seen.has(preset.id)) {
return false;
}
seen.add(preset.id);
return true;
});
}
function getAllMonsterPresets() {
return getMonsterPresetPool(null);
}
function uniqueText(values: Array<string | null | undefined>) {
return [
...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean)),
];
}
function buildMonsterSourceText(npc: CustomWorldMonsterSource) {
return uniqueText([
npc.name,
npc.title,
npc.role,
npc.description,
npc.backstory,
npc.personality,
npc.motivation,
npc.combatStyle,
...(npc.relationshipHooks ?? []),
...(npc.tags ?? []),
]).join(' ');
}
function buildSignalChars(label: string) {
return [
...new Set(
label
.replace(/[^\u4e00-\u9fa5]+/g, '')
.split('')
.filter((char) => char && !MONSTER_SIGNAL_STOP_CHARS.has(char)),
),
];
}
function scoreMonsterPreset(preset: HostileNpcPreset, sourceText: string) {
let score = 0;
if (sourceText.includes(preset.name)) {
score += 24;
}
for (const signalChar of buildSignalChars(preset.name)) {
if (sourceText.includes(signalChar)) {
score += 3;
}
}
for (const tag of [...preset.habitatTags, ...preset.combatTags]) {
if (tag && sourceText.includes(tag)) {
score += 2;
}
}
return score;
}
function scoreMonsterPresetWithArchetype(
preset: HostileNpcPreset,
sourceText: string,
options: {
archetypeSignals?: ReturnType<typeof collectCreatureArchetypeSignals> | null;
preferredWorldType?: WorldType | null;
} = {},
) {
let score = scoreMonsterPreset(preset, sourceText);
const { archetypeSignals, preferredWorldType } = options;
if (archetypeSignals) {
archetypeSignals.keywords.forEach((keyword) => {
if (!keyword) {
return;
}
if (
preset.name.includes(keyword)
|| preset.habitatTags.some((tag) => tag.includes(keyword) || keyword.includes(tag))
|| preset.combatTags.some((tag) => tag.includes(keyword) || keyword.includes(tag))
) {
score += keyword.length >= 3 ? 6 : 4;
}
});
archetypeSignals.combatTags.forEach((tag) => {
if (preset.combatTags.includes(tag)) {
score += 8;
}
});
archetypeSignals.habitatTags.forEach((tag) => {
if (preset.habitatTags.includes(tag)) {
score += 6;
}
});
}
if (
preferredWorldType
&& preferredWorldType !== WorldType.CUSTOM
&& preset.worldType === preferredWorldType
) {
score += 3;
}
return score;
}
export function getCustomWorldMonsterPresetPool(
profile?: Pick<
CustomWorldProfile,
'ownedSettingLayers' | 'templateWorldType' | 'compatibilityTemplateWorldType'
> | null,
) {
const presets = getAllMonsterPresets();
const creatureArchetypes =
profile?.ownedSettingLayers?.referenceProfile.creatureArchetypes ?? [];
if (creatureArchetypes.length === 0) {
return presets;
}
const preferredWorldType = profile
? resolveCustomWorldCompatibilityTemplateWorldType(profile)
: null;
const scoredPresets = presets
.map((preset) => {
const archetypeScore = creatureArchetypes.reduce((bestScore, archetype) => {
const nextScore = scoreMonsterPresetWithArchetype(
preset,
preset.name,
{
archetypeSignals: collectCreatureArchetypeSignals(archetype),
preferredWorldType,
},
);
return Math.max(bestScore, nextScore);
}, 0);
return {
preset,
score: archetypeScore,
};
})
.sort((left, right) => right.score - left.score);
const filtered = scoredPresets
.filter((entry) => entry.score > 0)
.map((entry) => entry.preset);
return filtered.length > 0 ? filtered : presets;
}
export function resolveCustomWorldNpcMonsterPreset(
npc: CustomWorldMonsterSource,
worldType?: WorldType | null,
profile?: Pick<
CustomWorldProfile,
'ownedSettingLayers' | 'templateWorldType' | 'compatibilityTemplateWorldType'
> | null,
) {
const sourceText = buildMonsterSourceText(npc);
if (!sourceText || !MONSTER_SIGNAL_PATTERN.test(sourceText)) {
return null;
}
const hostileBias = (npc.initialAffinity ?? 0) < 0;
if (!hostileBias) {
return null;
}
const preferredWorldType = profile
? resolveCustomWorldCompatibilityTemplateWorldType(profile)
: worldType ?? null;
const referenceArchetype = resolveCreatureArchetypeForSource(
profile as CustomWorldProfile | null | undefined,
npc,
);
const archetypeSignals = referenceArchetype
? collectCreatureArchetypeSignals(referenceArchetype)
: null;
const candidates =
profile && profile.ownedSettingLayers?.referenceProfile.creatureArchetypes.length
? getCustomWorldMonsterPresetPool(profile)
: getMonsterPresetPool(worldType);
if (candidates.length === 0) {
return null;
}
const scoredCandidates = candidates
.map((candidate) => ({
candidate,
score: scoreMonsterPresetWithArchetype(candidate, sourceText, {
archetypeSignals,
preferredWorldType,
}),
}))
.sort((left, right) => right.score - left.score);
if ((scoredCandidates[0]?.score ?? 0) >= 3) {
return scoredCandidates[0]?.candidate ?? null;
}
return candidates[hashText(sourceText) % candidates.length] ?? null;
}
export function resolveCustomWorldNpcMonsterPresetId(
npc: CustomWorldMonsterSource,
worldType?: WorldType | null,
profile?: Pick<
CustomWorldProfile,
'ownedSettingLayers' | 'templateWorldType' | 'compatibilityTemplateWorldType'
> | null,
) {
return resolveCustomWorldNpcMonsterPreset(npc, worldType, profile)?.id ?? null;
}