Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ The repository now has a workspace-based mod pipeline system:
### Built-in mods

- `example_wild_strawberry_hp_up`: changes Wild Strawberry HP recovery from `60` to `600`
- `2_3_choro_partset_skill_up`: generates the merged Choro support equipment overlays
- `soldoros_doll`: prepares the Soldoros doll APC and updates `aicharacter.lst`
- `2_3_choro_partset_skill_up`: generates the merged Choro support equipment overlays and consumes the prerequisite doll APC

### Built-in pipelines

- `wild-strawberry-only`: runs only the Wild Strawberry example mod
- `demo`: runs `example_wild_strawberry_hp_up -> 2_3_choro_partset_skill_up`
- `demo`: runs `example_wild_strawberry_hp_up -> soldoros_doll -> 2_3_choro_partset_skill_up`

`demo` is the current default pipeline.

Expand Down Expand Up @@ -116,6 +117,7 @@ If you want to test a temporary sequence without editing `mods/pipelines.ts`, pa
pnpm --filter pvf-mod-cli start build \
--pipeline adhoc-preview \
--mod example_wild_strawberry_hp_up \
--mod soldoros_doll \
--mod 2_3_choro_partset_skill_up
```

Expand Down
1 change: 1 addition & 0 deletions apps/pvf-mod-cli/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ test("runCli list shows registered pipelines and mods", async () => {
assert.match(output, /wild-strawberry-only/u);
assert.match(output, /demo/u);
assert.match(output, /example_wild_strawberry_hp_up/u);
assert.match(output, /soldoros_doll/u);
assert.match(output, /2_3_choro_partset_skill_up/u);
});

Expand Down
2 changes: 2 additions & 0 deletions docs/mod-pipeline-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ pipeline 层会在每个 mod 执行前后抓取 overlay 快照,计算该 mod
当前内建 mod 包括:

- `example_wild_strawberry_hp_up`
- `soldoros_doll`
- `2_3_choro_partset_skill_up`

当前内建 pipeline 包括:
Expand Down Expand Up @@ -352,6 +353,7 @@ CLI 支持重复传 `--mod` 来临时组装 pipeline。
pnpm --filter pvf-mod-cli start build \
--pipeline adhoc-preview \
--mod example_wild_strawberry_hp_up \
--mod soldoros_doll \
--mod 2_3_choro_partset_skill_up
```

Expand Down
3 changes: 1 addition & 2 deletions mods/2_3_choro_partset_skill_up/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export const TARGET_PIECE_COUNTS = new Set([3, 6, 9]);
export const EXPLAIN_HEADING = "获得以下套装的套装效果:";
export const GENERATED_SUPPORT_NAME_PREFIX = "诸界融核臂章";
export const GENERATED_SUPPORT_ID_START = 440453;
export const SUPPORT_SUMMON_SOURCE_NAME = "\u5251\u5723\u7d22\u5fb7\u7f57\u65af";
export const SUPPORT_SUMMON_DOLL_NAME = "\u7d22\u5fb7\u7f57\u65af";
export const SUPPORT_SUMMON_COOLDOWN = 900_000;
export const SUPPORT_SUMMON_EXPLAIN =
"↑↓+[宠物技能指令]输入时,可以召唤出剑圣索德罗斯协助自身战斗,剑圣索德罗斯存在15分钟。";
export const SUPPORT_SUMMON_ATTACK_DAMAGE_RATE = "1.0";
25 changes: 24 additions & 1 deletion mods/2_3_choro_partset_skill_up/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,26 @@ export async function loadSupportPathsByClass(
export async function findAiCharacterByName(
session: PvfModSession,
targetName: string,
options: {
pathIncludes?: string;
} = {},
): Promise<ListedPathEntry> {
const found = await tryFindAiCharacterByName(session, targetName, options);

if (found) {
return found;
}

throw new Error(`Unable to find APC id for ${targetName}.`);
}

export async function tryFindAiCharacterByName(
session: PvfModSession,
targetName: string,
options: {
pathIncludes?: string;
} = {},
): Promise<ListedPathEntry | undefined> {
const pathById = await loadListedPathById(
session,
AI_CHARACTER_LIST_PATH,
Expand All @@ -488,6 +507,10 @@ export async function findAiCharacterByName(
(left, right) => left[0] - right[0],
)
) {
if (options.pathIncludes && !aicPath.includes(options.pathIncludes)) {
continue;
}

const document = await readEquDocument(session, aicPath, session.textProfile);
const minimumInfoName = getFirstSectionString(document.children, "minimum info")?.trim();

Expand All @@ -499,7 +522,7 @@ export async function findAiCharacterByName(
}
}

throw new Error(`Unable to find APC id for ${targetName}.`);
return undefined;
}

export { findNextAvailableListedPathId };
137 changes: 128 additions & 9 deletions mods/2_3_choro_partset_skill_up/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,166 @@ import test from "node:test";

import { parseEquDocument } from "@pvf/equ-ast";
import { PvfArchive } from "@pvf/pvf-core";
import { applyPvfPipeline, buildPvfPipeline, createPvfModRegistry } from "@pvf/pvf-mod";
import {
applyPvfPipeline,
buildPvfPipeline,
createPvfModRegistry,
runPvfMods,
updateListedPathDocument,
} from "@pvf/pvf-mod";
import type { PvfMod } from "@pvf/pvf-mod";

import { CHORO_PARTSET_SKILL_UP_MOD_ID, choroPartsetSkillUpModDefinition } from "./index.ts";
import { AI_CHARACTER_LIST_PATH } from "./constants.ts";
import {
CHORO_PARTSET_SKILL_UP_MOD_ID,
choroPartsetSkillUpModDefinition,
createChoroPartsetSkillUpMod,
} from "./index.ts";
import type { ChoroPartsetSkillUpModSummary } from "./index.ts";
import {
SOLDOROS_DOLL_MOD_ID,
buildSupportSummonDollDocument,
soldorosDollModDefinition,
} from "../../soldoros_doll/src/index.ts";
import type { SoldorosDollModSummary } from "../../soldoros_doll/src/index.ts";

const FIXTURE_ARCHIVE_PATH = new URL("../../../fixtures/Script.pvf", import.meta.url).pathname;
const SOURCE_SUMMON_APC_PATH = "aicharacter/_jojochan/swordman/soldoros/soldoros.aic";
const TARGET_SWORDMAN_SUPPORT_PATH = "equipment/character/common/support/support_3choro65.equ";
const TARGET_SWORDMAN_OUTPUT_PATH = "equipment/character/common/support/support_440453.equ";
const TARGET_EXORCIST_SUPPORT_PATH = "equipment/character/common/support/support_3choro83.equ";
const TARGET_EXORCIST_OUTPUT_PATH = "equipment/character/common/support/support_440471.equ";
const TARGET_AVENGER_SUPPORT_PATH = "equipment/character/common/support/support_3choro84.equ";
const TARGET_AVENGER_OUTPUT_PATH = "equipment/character/common/support/support_440472.equ";
const EQUIPMENT_LIST_PATH = "equipment/equipment.lst";
const AI_CHARACTER_LIST_PATH = "aicharacter/aicharacter.lst";
const TARGET_SUMMON_APC_PATH = "aicharacter/_jojochan/swordman/soldoros/soldoros_doll.aic";
const TARGET_SUMMON_APC_ID = 1520;
const CUSTOM_SUMMON_APC_PATH = "aicharacter/_jojochan/swordman/soldoros/soldoros_custom_doll.aic";
const CUSTOM_SUMMON_APC_ID = 990_001;
const EXPECTED_FILE_COUNT = 24;
const EXPECTED_OVERLAY_COUNT = EXPECTED_FILE_COUNT + 3;

async function buildChoroPipeline() {
const result = await buildPvfPipeline({
archivePath: FIXTURE_ARCHIVE_PATH,
registry: createPvfModRegistry([choroPartsetSkillUpModDefinition]),
registry: createPvfModRegistry([
soldorosDollModDefinition,
choroPartsetSkillUpModDefinition,
]),
pipeline: {
id: "choro-only",
mods: [
{
id: SOLDOROS_DOLL_MOD_ID,
},
{
id: CHORO_PARTSET_SKILL_UP_MOD_ID,
},
],
},
});
const summary = result.mods[0]?.result as ChoroPartsetSkillUpModSummary | undefined;
const soldorosSummary = result.mods[0]?.result as SoldorosDollModSummary | undefined;
const summary = result.mods[1]?.result as ChoroPartsetSkillUpModSummary | undefined;

if (!soldorosSummary) {
throw new Error("Missing Soldoros prerequisite summary.");
}

if (!summary) {
throw new Error("Missing Choro pipeline summary.");
}

return {
result,
soldorosSummary,
summary,
};
}

test("choro support mod omits summon command when the doll prerequisite is missing", async () => {
const result = await buildPvfPipeline({
archivePath: FIXTURE_ARCHIVE_PATH,
registry: createPvfModRegistry([choroPartsetSkillUpModDefinition]),
pipeline: {
id: "choro-without-prerequisite",
mods: [
{
id: CHORO_PARTSET_SKILL_UP_MOD_ID,
},
],
},
});
const summary = result.mods[0]?.result as ChoroPartsetSkillUpModSummary | undefined;

if (!summary) {
throw new Error("Missing Choro summary without prerequisite.");
}

const swordmanOverlay = result.overlays.find(
(overlay) => overlay.path === TARGET_SWORDMAN_OUTPUT_PATH,
);

assert.equal(summary.files.length, EXPECTED_FILE_COUNT);
assert.equal(summary.skipped.length, 0);
assert.ok(swordmanOverlay);
assert.ok(result.overlays.some((overlay) => overlay.path === EQUIPMENT_LIST_PATH));
assert.ok(!result.overlays.some((overlay) => overlay.path === AI_CHARACTER_LIST_PATH));
assert.ok(!result.overlays.some((overlay) => overlay.path === TARGET_SUMMON_APC_PATH));
assert.match(String(swordmanOverlay.content), /\[explain\]/u);
assert.match(String(swordmanOverlay.content), /获得以下套装的套装效果:/u);
assert.doesNotMatch(
String(swordmanOverlay.content),
/剑圣索德罗斯协助自身战斗,剑圣索德罗斯存在15分钟。/u,
);
assert.doesNotMatch(String(swordmanOverlay.content), /\[command\]/u);
assert.doesNotMatch(String(swordmanOverlay.content), /\[summon apc\]/u);
});

test("choro support mod discovers the summon APC id from previous mod overlays", async () => {
const prerequisiteMod: PvfMod = {
id: "inject-custom-soldoros-doll",
async apply(session) {
const aiCharacterListDocument = await session.readScriptDocument(AI_CHARACTER_LIST_PATH);
const sourceSummonDocument = await session.readScriptDocument(SOURCE_SUMMON_APC_PATH);

session.writeScriptDocument(
CUSTOM_SUMMON_APC_PATH,
buildSupportSummonDollDocument(sourceSummonDocument),
);
session.writeScriptDocument(
AI_CHARACTER_LIST_PATH,
updateListedPathDocument(
aiCharacterListDocument,
"aicharacter",
[
{
id: CUSTOM_SUMMON_APC_ID,
path: CUSTOM_SUMMON_APC_PATH,
},
],
),
);
},
};
const result = await runPvfMods({
archivePath: FIXTURE_ARCHIVE_PATH,
mods: [prerequisiteMod, createChoroPartsetSkillUpMod()],
});
const swordmanOverlay = result.overlays.find(
(overlay) => overlay.path === TARGET_SWORDMAN_OUTPUT_PATH,
);

assert.ok(swordmanOverlay);
assert.match(
String(swordmanOverlay.content),
new RegExp(`\\[summon apc\\]\\r?\\n${CUSTOM_SUMMON_APC_ID}\\t99\\t1`, "u"),
);
});

test("choro mod builds generated support overlays through the pipeline", async () => {
const { result, summary } = await buildChoroPipeline();
const { result, soldorosSummary, summary } = await buildChoroPipeline();

assert.equal(soldorosSummary.dollAicId, TARGET_SUMMON_APC_ID);
assert.equal(summary.files.length, EXPECTED_FILE_COUNT);
assert.equal(result.overlays.length, EXPECTED_OVERLAY_COUNT);
assert.equal(summary.skipped.length, 0);
Expand Down Expand Up @@ -104,7 +217,7 @@ test("choro mod builds generated support overlays through the pipeline", async (
);

assert.ok(summonApcOverlay);
assert.match(String(summonApcOverlay.content), /剑圣索德罗斯/u);
assert.match(String(summonApcOverlay.content), /\[minimum info\][\s\S]*索德罗斯/u);
assert.match(String(summonApcOverlay.content), /\[attack damage rate\]\r?\n1\.0/u);

const swordmanDocument = parseEquDocument(String(swordmanOverlay.content));
Expand All @@ -128,10 +241,16 @@ test("choro mod applies cleanly through the pipeline", async () => {
const result = await applyPvfPipeline({
archivePath: FIXTURE_ARCHIVE_PATH,
outputPath,
registry: createPvfModRegistry([choroPartsetSkillUpModDefinition]),
registry: createPvfModRegistry([
soldorosDollModDefinition,
choroPartsetSkillUpModDefinition,
]),
pipeline: {
id: "choro-only",
mods: [
{
id: SOLDOROS_DOLL_MOD_ID,
},
{
id: CHORO_PARTSET_SKILL_UP_MOD_ID,
},
Expand Down Expand Up @@ -179,7 +298,7 @@ test("choro mod applies cleanly through the pipeline", async () => {
content,
new RegExp(`\\[summon apc\\]\\r?\\n${TARGET_SUMMON_APC_ID}\\t99\\t1`, "u"),
);
assert.match(summonApcText, /剑圣索德罗斯/u);
assert.match(summonApcText, /\[minimum info\][\s\S]*索德罗斯/u);
assert.match(summonApcText, /\[attack damage rate\]\r?\n1\.0/u);
assert.match(
aiCharacterListText,
Expand Down
1 change: 0 additions & 1 deletion mods/2_3_choro_partset_skill_up/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {
} from "./types.ts";

export const CHORO_PARTSET_SKILL_UP_MOD_ID = "2_3_choro_partset_skill_up";

export const choroPartsetSkillUpModDefinition: PvfRegisteredMod<
undefined,
ChoroPartsetSkillUpModSummary
Expand Down
Loading
Loading