merge: 최신 main을 게임 버전 표시에 재통합
This commit is contained in:
@@ -5,6 +5,16 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
import { loadScenarioDefinitionById, resolveScenarioDefaultsPath } from '../src/scenario/scenarioLoader.js';
|
import { loadScenarioDefinitionById, resolveScenarioDefaultsPath } from '../src/scenario/scenarioLoader.js';
|
||||||
|
|
||||||
|
type LoadedScenario = Awaited<ReturnType<typeof loadScenarioDefinitionById>>;
|
||||||
|
|
||||||
|
const readItemSlot = (scenario: LoadedScenario, slot: string): Record<string, number> => {
|
||||||
|
const allItems = scenario.config.const.allItems as Record<string, Record<string, number>> | undefined;
|
||||||
|
return allItems?.[slot] ?? {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const readAvailableSpecialWar = (scenario: LoadedScenario): string[] =>
|
||||||
|
(scenario.config.const.availableSpecialWar as string[] | undefined) ?? [];
|
||||||
|
|
||||||
describe('tracked scenario resources', () => {
|
describe('tracked scenario resources', () => {
|
||||||
it('loads every scenario through its composed resource graph', async () => {
|
it('loads every scenario through its composed resource graph', async () => {
|
||||||
const scenarioRoot = path.dirname(resolveScenarioDefaultsPath());
|
const scenarioRoot = path.dirname(resolveScenarioDefaultsPath());
|
||||||
@@ -35,4 +45,36 @@ describe('tracked scenario resources', () => {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the buyable war-special pack scoped to each Ref scenario contract', async () => {
|
||||||
|
const [ordinaryBlank, legacySecretBlank, mirrorBlank, multiUnitBlank, moreEffectBlank, composedAddon] =
|
||||||
|
await Promise.all(
|
||||||
|
[0, 902, 910, 912, 913, 2141].map((scenarioId) => loadScenarioDefinitionById(scenarioId))
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Object.keys(readItemSlot(ordinaryBlank, 'item')).filter((key) => key.startsWith('event_전투특기_'))
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
const legacySecretItems = readItemSlot(legacySecretBlank, 'item');
|
||||||
|
expect(Object.keys(legacySecretItems).filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(19);
|
||||||
|
expect(legacySecretItems).not.toHaveProperty('event_전투특기_견고');
|
||||||
|
expect(readAvailableSpecialWar(legacySecretBlank)).not.toContain('che_견고');
|
||||||
|
|
||||||
|
const mirrorItems = readItemSlot(mirrorBlank, 'item');
|
||||||
|
expect(Object.keys(mirrorItems).filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(19);
|
||||||
|
expect(mirrorItems).not.toHaveProperty('event_전투특기_척사');
|
||||||
|
expect(readAvailableSpecialWar(mirrorBlank)).not.toContain('che_척사');
|
||||||
|
|
||||||
|
const multiUnitItems = readItemSlot(multiUnitBlank, 'item');
|
||||||
|
expect(Object.keys(multiUnitItems).filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(19);
|
||||||
|
expect(multiUnitItems).not.toHaveProperty('event_전투특기_견고');
|
||||||
|
|
||||||
|
const moreEffectItems = readItemSlot(moreEffectBlank, 'item');
|
||||||
|
const composedAddonItems = readItemSlot(composedAddon, 'item');
|
||||||
|
expect(Object.keys(moreEffectItems).filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(20);
|
||||||
|
expect(Object.keys(composedAddonItems).filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(20);
|
||||||
|
expect(readItemSlot(moreEffectBlank, 'horse').che_명마_07_백마).toBe(4);
|
||||||
|
expect(readItemSlot(composedAddon, 'horse').che_명마_07_백마).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -93,6 +93,49 @@ const canRun = await canConnectToDatabase(databaseUrl);
|
|||||||
const describeDb = describe.runIf(canRun);
|
const describeDb = describe.runIf(canRun);
|
||||||
|
|
||||||
describeDb('scenario database seed', () => {
|
describeDb('scenario database seed', () => {
|
||||||
|
test('persists each blank-land scenario item contract without leaking the shared addon', async () => {
|
||||||
|
const readPersistedItemContract = async (targetScenarioId: number) => {
|
||||||
|
const { applied } = await seedScenarioToDatabase({
|
||||||
|
scenarioId: targetScenarioId,
|
||||||
|
databaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
||||||
|
await connector.connect();
|
||||||
|
try {
|
||||||
|
const worldState = await connector.prisma.worldState.findFirstOrThrow();
|
||||||
|
const config = worldState.config as Record<string, unknown>;
|
||||||
|
const scenarioConst = (config.const ?? {}) as Record<string, unknown>;
|
||||||
|
const allItems = (scenarioConst.allItems ?? {}) as Record<string, Record<string, number>>;
|
||||||
|
const items = allItems.item ?? {};
|
||||||
|
const availableSpecialWar = (scenarioConst.availableSpecialWar ?? []) as string[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
applied,
|
||||||
|
battleTraitItemCount: Object.keys(items).filter((key) => key.startsWith('event_전투특기_')).length,
|
||||||
|
availableSpecialWar,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await connector.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ordinaryBlank = await readPersistedItemContract(0);
|
||||||
|
const legacySecretBlank = await readPersistedItemContract(902);
|
||||||
|
|
||||||
|
expect(ordinaryBlank).toMatchObject({
|
||||||
|
applied: true,
|
||||||
|
battleTraitItemCount: 0,
|
||||||
|
availableSpecialWar: [],
|
||||||
|
});
|
||||||
|
expect(legacySecretBlank.applied).toBe(true);
|
||||||
|
expect(legacySecretBlank.battleTraitItemCount).toBe(19);
|
||||||
|
expect(legacySecretBlank.availableSpecialWar).toHaveLength(19);
|
||||||
|
expect(legacySecretBlank.items).not.toHaveProperty('event_전투특기_견고');
|
||||||
|
expect(legacySecretBlank.availableSpecialWar).not.toContain('che_견고');
|
||||||
|
});
|
||||||
|
|
||||||
test('snapshots the complete opening inheritance balance before game activity', async () => {
|
test('snapshots the complete opening inheritance balance before game activity', async () => {
|
||||||
const serverId = 'scenario-seeder-inheritance-baseline';
|
const serverId = 'scenario-seeder-inheritance-baseline';
|
||||||
const userId = 'scenario-seeder-inheritance-user';
|
const userId = 'scenario-seeder-inheritance-user';
|
||||||
|
|||||||
@@ -58,6 +58,26 @@
|
|||||||
시나리오 80개 중 70개가 확장을 사용합니다. 구매 가능한 전특·유니크 표를
|
시나리오 80개 중 70개가 확장을 사용합니다. 구매 가능한 전특·유니크 표를
|
||||||
사용하는 10개 시나리오는 같은 item 확장을 참조합니다.
|
사용하는 10개 시나리오는 같은 item 확장을 참조합니다.
|
||||||
|
|
||||||
|
## 적용 범위와 Ref 차이
|
||||||
|
|
||||||
|
Ref에는 설치 시 선택한 시나리오에 별도 기능 팩을 덧붙이는 전역 애드온 단계가
|
||||||
|
없습니다. 각 `scenario_*.json`이 `const.allItems`와
|
||||||
|
`const.availableSpecialWar`를 직접 소유하고, `Scenario::buildConf()`가 그 값을
|
||||||
|
`GameConst`에 반영합니다.
|
||||||
|
|
||||||
|
Core의 `extends`는 이 중복 값을 소스에서 재사용하기 위한 합성 기능입니다.
|
||||||
|
설치 시 임의의 시나리오에 전역으로 적용되는 옵션이 아니며, 해당
|
||||||
|
`scenario_*.json`이 확장을 명시한 경우에만 로더와 Gateway 미리보기가 합성합니다.
|
||||||
|
따라서 일반 공백지 시나리오에는
|
||||||
|
`extensions/items/buyable-war-special-uniques.json`이 암묵적으로 적용되지 않습니다.
|
||||||
|
|
||||||
|
공백지 중 `scenario_902`(천지비급), `scenario_910`(거울세계),
|
||||||
|
`scenario_912`(다병종), `scenario_913`(무한대흥)은 Ref 자체가 전투 특기 아이템
|
||||||
|
풀을 직접 정의합니다. 이 네 시나리오는 최신 공통 확장과 항목 또는 유니크 수량이
|
||||||
|
서로 달라 직접 정의를 유지합니다. 특히 902·912는 `견고`가 없는 19종, 910은
|
||||||
|
`척사`가 없는 19종이며, 913은 20종이지만 일부 유니크 수량이 공통 확장의 2개가
|
||||||
|
아닌 4개입니다. 이를 공통 확장으로 바꾸면 Ref 설치 결과가 달라집니다.
|
||||||
|
|
||||||
## 검증
|
## 검증
|
||||||
|
|
||||||
확장 파일을 추가하거나 합성 순서를 바꾼 뒤 다음 검사를 실행해 주세요.
|
확장 파일을 추가하거나 합성 순서를 바꾼 뒤 다음 검사를 실행해 주세요.
|
||||||
|
|||||||
Reference in New Issue
Block a user