endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
91 lines • 3.73 kB
JavaScript
/**
* Character profile (archives) and voice-line reader.
*
* This is the **fan-creation core** of the character domain — archives
* give the background-story text a writer needs, voice lines give the
* speech patterns. The numeric fields (profession, rarity, weaponType)
* live in characters.ts:getCharacterInfo for the gameplay-curious.
*
* Both profileRecord and profileVoice live inside CharacterTable.json
* (unlike PRTS-MCP where they're in separate handbook/charword tables),
* so this module reuses characters.ts's table loader and resolver.
*
* ## Text cleaning
*
* Endfield's profile text contains rich-text tags like `<@profile.key>`
* (key markers) and `</>` (close markers). These are display hints, not
* content — we strip them so the LLM gets clean prose. Voice lines are
* typically plain text without tags.
*/
import { resolveCharacterEntry } from "./characters.js";
import { resolveText, } from "./texts.js";
// ---------------------------------------------------------------------------
// Text cleaning
// ---------------------------------------------------------------------------
/**
* Strip Endfield rich-text markup from profile/voice text.
*
* Two opening-tag families exist in the data (both closed by `</>`):
* <@profile.key>...</> — key/highlight span (98+ distinct tags)
* <#ba.consume>...</> — status/effect span (98 distinct tags, 870+ values)
*
* Both are display hints, not content. We strip them so the LLM gets
* clean prose. The close tag `</>` is shared across both families.
*/
export function cleanProfileText(text) {
// Drop opening tags: <@...> and <#...> (the two Endfield tag families).
let out = text.replace(/<[#@][a-zA-Z0-9_.]+>/g, "");
// Drop close tags </>
out = out.replace(/<\/>/g, "");
return out.trim();
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Get a character's archive sections (background story text).
*
* Returns 3 sections per character (basic profile / personnel summary /
* archive material), each with a title and full prose body. Text is
* cleaned of rich-text tags and localized to the requested language.
*
* Returns null if the character is not found or has no archive data.
*/
export function getCharacterArchives(idOrName, lang = "CN") {
const resolved = resolveCharacterEntry(idOrName, lang);
if (resolved === null)
return null;
const records = resolved.entry
.profileRecord;
if (!records || records.length === 0)
return null;
return records.map((r) => ({
recordId: r.recordID ?? "",
title: resolveText(r.recordTitle, lang, r.recordID ?? ""),
text: cleanProfileText(resolveText(r.recordDesc, lang, "")),
}));
}
/**
* Get a character's voice lines (speech text with trigger conditions).
*
* Returns ~55 lines per character, each with an index, a trigger title
* (e.g. "行动准备1", "编入队伍1"), and the spoken text. Localized to
* the requested language.
*
* Returns null if the character is not found or has no voice data.
*/
export function getCharacterVoices(idOrName, lang = "CN") {
const resolved = resolveCharacterEntry(idOrName, lang);
if (resolved === null)
return null;
const voices = resolved.entry
.profileVoice;
if (!voices || voices.length === 0)
return null;
return voices.map((v) => ({
index: v.voiceIndex ?? 0,
title: resolveText(v.voiceTitle, lang, v.voId ?? ""),
text: cleanProfileText(resolveText(v.voiceDesc, lang, "")),
}));
}
//# sourceMappingURL=characterProfiles.js.map