feat: make Xuzhou choices shape battle eight
This commit is contained in:
@@ -24,6 +24,7 @@ export type CityResonanceDialogueChoice = {
|
||||
label: string;
|
||||
response: string;
|
||||
rewardExp: number;
|
||||
battleEffect?: string;
|
||||
};
|
||||
|
||||
export type CityResonanceDialogue = {
|
||||
@@ -70,8 +71,8 @@ export const cityStayDefinitions: readonly CityStayDefinition[] = [
|
||||
location: '서주 관청',
|
||||
description: '관청의 창고 장부와 상인들의 증언을 맞추어 소패로 향하는 보급로의 약한 지점을 찾습니다.',
|
||||
briefingLines: [
|
||||
'소패의 창고는 서쪽 길목과 마을 어귀를 통해 보급을 받습니다.',
|
||||
'습격대보다 먼저 마을과 창고 주변을 안정시키면 고순의 압박을 견디기 쉬워집니다.'
|
||||
'창고 보급은 서쪽 길목으로 들어오며, 장부의 빈틈을 노린 여포군 기병은 동쪽 길에서 창고를 우회 압박합니다.',
|
||||
'마을 어귀와 창고 습격대의 움직임을 배치 전에 확인해 먼저 대응하십시오.'
|
||||
],
|
||||
itemReward: '소패 보급로 장부 1'
|
||||
},
|
||||
@@ -90,13 +91,15 @@ export const cityStayDefinitions: readonly CityStayDefinition[] = [
|
||||
id: 'city-xuzhou-share-storehouse-duty',
|
||||
label: '창고와 백성의 몫을 함께 지킨다',
|
||||
response: '미축은 군량과 구휼 장부를 나누어 적고, 어느 쪽도 희생시키지 않겠다고 답했습니다.',
|
||||
rewardExp: 10
|
||||
rewardExp: 12,
|
||||
battleEffect: '8차 전투 3턴까지 미축의 첫 ‘응급’ 실제 회복량 +4'
|
||||
},
|
||||
{
|
||||
id: 'city-xuzhou-trust-mi-zhu-ledger',
|
||||
label: '보급 판단을 미축에게 맡긴다',
|
||||
response: '미축은 유비의 신뢰에 고개를 숙이고 소패까지 이어지는 상단과 창고를 직접 정리했습니다.',
|
||||
rewardExp: 12
|
||||
rewardExp: 12,
|
||||
battleEffect: '8차 전투 3턴까지 미축의 첫 ‘격려’ 지속 +1턴'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
511
src/game/data/xuzhouStayBattlePayoff.ts
Normal file
511
src/game/data/xuzhouStayBattlePayoff.ts
Normal file
@@ -0,0 +1,511 @@
|
||||
export const xuzhouStayBattlePayoffVersion = 1 as const;
|
||||
export const xuzhouStayBattlePayoffRecordId =
|
||||
'xuzhou-stay-battle-payoff' as const;
|
||||
export const xuzhouStayId = 'xuzhou' as const;
|
||||
export const xuzhouStayBattlePayoffSourceBattleId =
|
||||
'seventh-battle-xuzhou-rescue' as const;
|
||||
export const xuzhouStayBattlePayoffTargetBattleId =
|
||||
'eighth-battle-xiaopei-supply-road' as const;
|
||||
export const xuzhouStayInformationVisitId =
|
||||
'city-xuzhou-xiaopei-ledger' as const;
|
||||
export const xuzhouStayResonanceDialogueId =
|
||||
'city-xuzhou-liu-mi-supply-trust' as const;
|
||||
export const xuzhouStayShareStorehouseDutyChoiceId =
|
||||
'city-xuzhou-share-storehouse-duty' as const;
|
||||
export const xuzhouStayTrustMiZhuLedgerChoiceId =
|
||||
'city-xuzhou-trust-mi-zhu-ledger' as const;
|
||||
export const xuzhouStayMiZhuUnitId = 'mi-zhu' as const;
|
||||
export const xuzhouStayBattlePayoffActiveThroughTurn = 3 as const;
|
||||
export const xuzhouStayInformationEnemyUnitIds = [
|
||||
'xiaopei-guard-a',
|
||||
'xiaopei-guard-b'
|
||||
] as const;
|
||||
|
||||
export const xuzhouStayBattlePayoffChoiceIds = [
|
||||
xuzhouStayShareStorehouseDutyChoiceId,
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
] as const;
|
||||
|
||||
export type XuzhouStayBattlePayoffChoiceId =
|
||||
(typeof xuzhouStayBattlePayoffChoiceIds)[number];
|
||||
|
||||
export type XuzhouStayInformationEffect = {
|
||||
kind: 'deployment-intent-preview';
|
||||
enemyUnitIds: readonly string[];
|
||||
previewCount: 2;
|
||||
};
|
||||
|
||||
export type XuzhouStayAidEffect = {
|
||||
kind: 'mi-zhu-first-aid-healing';
|
||||
actorUnitId: typeof xuzhouStayMiZhuUnitId;
|
||||
usableId: 'aid';
|
||||
activeThroughTurn: typeof xuzhouStayBattlePayoffActiveThroughTurn;
|
||||
bonusHealing: 4;
|
||||
maxBattleUses: 1;
|
||||
};
|
||||
|
||||
export type XuzhouStayEncourageEffect = {
|
||||
kind: 'mi-zhu-first-encourage-duration';
|
||||
actorUnitId: typeof xuzhouStayMiZhuUnitId;
|
||||
usableId: 'encourage';
|
||||
activeThroughTurn: typeof xuzhouStayBattlePayoffActiveThroughTurn;
|
||||
bonusBuffTurns: 1;
|
||||
maxBattleUses: 1;
|
||||
};
|
||||
|
||||
export type XuzhouStayResonanceEffect =
|
||||
| XuzhouStayAidEffect
|
||||
| XuzhouStayEncourageEffect;
|
||||
|
||||
export const xuzhouStayBattlePayoffDefinition = {
|
||||
version: xuzhouStayBattlePayoffVersion,
|
||||
recordId: xuzhouStayBattlePayoffRecordId,
|
||||
cityStayId: xuzhouStayId,
|
||||
sourceBattleId: xuzhouStayBattlePayoffSourceBattleId,
|
||||
targetBattleId: xuzhouStayBattlePayoffTargetBattleId,
|
||||
information: {
|
||||
visitId: xuzhouStayInformationVisitId,
|
||||
effect: {
|
||||
kind: 'deployment-intent-preview',
|
||||
enemyUnitIds: xuzhouStayInformationEnemyUnitIds,
|
||||
previewCount: 2
|
||||
} satisfies XuzhouStayInformationEffect
|
||||
},
|
||||
dialogue: {
|
||||
dialogueId: xuzhouStayResonanceDialogueId,
|
||||
choices: {
|
||||
[xuzhouStayShareStorehouseDutyChoiceId]: {
|
||||
kind: 'mi-zhu-first-aid-healing',
|
||||
actorUnitId: xuzhouStayMiZhuUnitId,
|
||||
usableId: 'aid',
|
||||
activeThroughTurn:
|
||||
xuzhouStayBattlePayoffActiveThroughTurn,
|
||||
bonusHealing: 4,
|
||||
maxBattleUses: 1
|
||||
} satisfies XuzhouStayAidEffect,
|
||||
[xuzhouStayTrustMiZhuLedgerChoiceId]: {
|
||||
kind: 'mi-zhu-first-encourage-duration',
|
||||
actorUnitId: xuzhouStayMiZhuUnitId,
|
||||
usableId: 'encourage',
|
||||
activeThroughTurn:
|
||||
xuzhouStayBattlePayoffActiveThroughTurn,
|
||||
bonusBuffTurns: 1,
|
||||
maxBattleUses: 1
|
||||
} satisfies XuzhouStayEncourageEffect
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type XuzhouStayBattleRecord = {
|
||||
battleId?: unknown;
|
||||
outcome?: unknown;
|
||||
};
|
||||
|
||||
export type XuzhouStayBattlePayoffCampaignState = {
|
||||
battleHistory?: Readonly<
|
||||
Record<string, XuzhouStayBattleRecord | null | undefined>
|
||||
>;
|
||||
firstBattleReport?: XuzhouStayBattleRecord | null;
|
||||
completedCampVisits?: readonly unknown[];
|
||||
completedCampDialogues?: readonly unknown[];
|
||||
campDialogueChoiceIds?: Readonly<Record<string, unknown>> | null;
|
||||
};
|
||||
|
||||
export type XuzhouStayBattlePayoffRecordSource =
|
||||
| 'battle-history'
|
||||
| 'legacy-report';
|
||||
|
||||
export type ResolvedXuzhouStayBattlePayoff = {
|
||||
recordId: typeof xuzhouStayBattlePayoffRecordId;
|
||||
cityStayId: typeof xuzhouStayId;
|
||||
sourceBattleId: typeof xuzhouStayBattlePayoffSourceBattleId;
|
||||
targetBattleId: typeof xuzhouStayBattlePayoffTargetBattleId;
|
||||
informationVisitId: typeof xuzhouStayInformationVisitId;
|
||||
dialogueId: typeof xuzhouStayResonanceDialogueId;
|
||||
recordSource: XuzhouStayBattlePayoffRecordSource;
|
||||
informationCompleted: boolean;
|
||||
informationEffect?: XuzhouStayInformationEffect;
|
||||
choiceId?: XuzhouStayBattlePayoffChoiceId;
|
||||
resonanceEffect?: XuzhouStayResonanceEffect;
|
||||
};
|
||||
|
||||
export type XuzhouStayBattlePayoffSnapshot = {
|
||||
version: typeof xuzhouStayBattlePayoffVersion;
|
||||
recordId: typeof xuzhouStayBattlePayoffRecordId;
|
||||
cityStayId: typeof xuzhouStayId;
|
||||
sourceBattleId: typeof xuzhouStayBattlePayoffSourceBattleId;
|
||||
targetBattleId: typeof xuzhouStayBattlePayoffTargetBattleId;
|
||||
informationVisitId: typeof xuzhouStayInformationVisitId;
|
||||
dialogueId: typeof xuzhouStayResonanceDialogueId;
|
||||
informationCompleted: boolean;
|
||||
choiceId?: XuzhouStayBattlePayoffChoiceId;
|
||||
informationCounterplays: number;
|
||||
miZhuDeployed: boolean;
|
||||
supportAttempts: number;
|
||||
supportUses: number;
|
||||
bonusHealing: number;
|
||||
bonusBuffTurns: number;
|
||||
};
|
||||
|
||||
export type XuzhouStayBattlePayoffRuntime =
|
||||
XuzhouStayBattlePayoffSnapshot;
|
||||
export type XuzhouStayBattlePayoffRuntimeState =
|
||||
XuzhouStayBattlePayoffRuntime;
|
||||
|
||||
export type XuzhouStayBattlePayoffNormalizationOptions = {
|
||||
expectedBattleId?: unknown;
|
||||
expectedRosterUnitIds?: readonly unknown[] | ReadonlySet<string>;
|
||||
expectedMemory?: ResolvedXuzhouStayBattlePayoff | null;
|
||||
};
|
||||
|
||||
export function isXuzhouStayBattlePayoffChoiceId(
|
||||
value: unknown
|
||||
): value is XuzhouStayBattlePayoffChoiceId {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
(xuzhouStayBattlePayoffChoiceIds as readonly string[]).includes(
|
||||
value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function getXuzhouStayResonanceEffect(
|
||||
choiceId: unknown
|
||||
): XuzhouStayResonanceEffect | undefined {
|
||||
if (!isXuzhouStayBattlePayoffChoiceId(choiceId)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...xuzhouStayBattlePayoffDefinition.dialogue.choices[choiceId]
|
||||
};
|
||||
}
|
||||
|
||||
export function isXuzhouStayBattlePayoffActiveTurn(
|
||||
turnNumber: unknown
|
||||
) {
|
||||
return (
|
||||
typeof turnNumber === 'number' &&
|
||||
Number.isInteger(turnNumber) &&
|
||||
turnNumber >= 1 &&
|
||||
turnNumber <= xuzhouStayBattlePayoffActiveThroughTurn
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveXuzhouStayBattlePayoff(
|
||||
campaign?: XuzhouStayBattlePayoffCampaignState | null,
|
||||
battleId: unknown = xuzhouStayBattlePayoffTargetBattleId
|
||||
): ResolvedXuzhouStayBattlePayoff | undefined {
|
||||
if (battleId !== xuzhouStayBattlePayoffTargetBattleId) {
|
||||
return undefined;
|
||||
}
|
||||
const recordSource = selectSourceVictoryRecord(campaign);
|
||||
if (!recordSource) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const informationCompleted = arrayValue(
|
||||
campaign?.completedCampVisits
|
||||
).includes(xuzhouStayInformationVisitId);
|
||||
const dialogueCompleted = arrayValue(
|
||||
campaign?.completedCampDialogues
|
||||
).includes(xuzhouStayResonanceDialogueId);
|
||||
const rawChoiceId = isRecord(campaign?.campDialogueChoiceIds)
|
||||
? campaign.campDialogueChoiceIds[
|
||||
xuzhouStayResonanceDialogueId
|
||||
]
|
||||
: undefined;
|
||||
const choiceId =
|
||||
dialogueCompleted &&
|
||||
isXuzhouStayBattlePayoffChoiceId(rawChoiceId)
|
||||
? rawChoiceId
|
||||
: undefined;
|
||||
|
||||
if (!informationCompleted && !choiceId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
recordId: xuzhouStayBattlePayoffRecordId,
|
||||
cityStayId: xuzhouStayId,
|
||||
sourceBattleId: xuzhouStayBattlePayoffSourceBattleId,
|
||||
targetBattleId: xuzhouStayBattlePayoffTargetBattleId,
|
||||
informationVisitId: xuzhouStayInformationVisitId,
|
||||
dialogueId: xuzhouStayResonanceDialogueId,
|
||||
recordSource,
|
||||
informationCompleted,
|
||||
...(informationCompleted
|
||||
? {
|
||||
informationEffect: {
|
||||
...xuzhouStayBattlePayoffDefinition.information.effect,
|
||||
enemyUnitIds: [
|
||||
...xuzhouStayInformationEnemyUnitIds
|
||||
]
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(choiceId
|
||||
? {
|
||||
choiceId,
|
||||
resonanceEffect:
|
||||
getXuzhouStayResonanceEffect(choiceId)!
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
export function createXuzhouStayBattlePayoffRuntime(
|
||||
memory: ResolvedXuzhouStayBattlePayoff,
|
||||
options: {
|
||||
miZhuDeployed: unknown;
|
||||
expectedRosterUnitIds?: readonly unknown[] | ReadonlySet<string>;
|
||||
}
|
||||
): XuzhouStayBattlePayoffRuntime | undefined {
|
||||
return normalizeXuzhouStayBattlePayoffRuntime(
|
||||
{
|
||||
version: xuzhouStayBattlePayoffVersion,
|
||||
recordId: memory.recordId,
|
||||
cityStayId: memory.cityStayId,
|
||||
sourceBattleId: memory.sourceBattleId,
|
||||
targetBattleId: memory.targetBattleId,
|
||||
informationVisitId: memory.informationVisitId,
|
||||
dialogueId: memory.dialogueId,
|
||||
informationCompleted: memory.informationCompleted,
|
||||
...(memory.choiceId ? { choiceId: memory.choiceId } : {}),
|
||||
informationCounterplays: 0,
|
||||
miZhuDeployed: options.miZhuDeployed,
|
||||
supportAttempts: 0,
|
||||
supportUses: 0,
|
||||
bonusHealing: 0,
|
||||
bonusBuffTurns: 0
|
||||
},
|
||||
{
|
||||
expectedBattleId: memory.targetBattleId,
|
||||
expectedRosterUnitIds: options.expectedRosterUnitIds,
|
||||
expectedMemory: memory
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeXuzhouStayBattlePayoffRuntime(
|
||||
value: unknown,
|
||||
options: XuzhouStayBattlePayoffNormalizationOptions = {}
|
||||
): XuzhouStayBattlePayoffRuntime | undefined {
|
||||
return normalizePayoffState(value, options);
|
||||
}
|
||||
|
||||
export function cloneXuzhouStayBattlePayoffRuntime(
|
||||
runtime: XuzhouStayBattlePayoffRuntime
|
||||
): XuzhouStayBattlePayoffRuntime {
|
||||
return { ...runtime };
|
||||
}
|
||||
|
||||
export function createXuzhouStayBattlePayoffSnapshot(
|
||||
runtime: unknown,
|
||||
options: XuzhouStayBattlePayoffNormalizationOptions = {}
|
||||
): XuzhouStayBattlePayoffSnapshot | undefined {
|
||||
return normalizeXuzhouStayBattlePayoffSnapshot(runtime, options);
|
||||
}
|
||||
|
||||
export function normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
value: unknown,
|
||||
options: XuzhouStayBattlePayoffNormalizationOptions = {}
|
||||
): XuzhouStayBattlePayoffSnapshot | undefined {
|
||||
return normalizePayoffState(value, options);
|
||||
}
|
||||
|
||||
export function cloneXuzhouStayBattlePayoffSnapshot(
|
||||
snapshot: XuzhouStayBattlePayoffSnapshot
|
||||
): XuzhouStayBattlePayoffSnapshot {
|
||||
return { ...snapshot };
|
||||
}
|
||||
|
||||
function normalizePayoffState(
|
||||
value: unknown,
|
||||
options: XuzhouStayBattlePayoffNormalizationOptions
|
||||
): XuzhouStayBattlePayoffSnapshot | undefined {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
value.version !== xuzhouStayBattlePayoffVersion ||
|
||||
value.recordId !== xuzhouStayBattlePayoffRecordId ||
|
||||
value.cityStayId !== xuzhouStayId ||
|
||||
value.sourceBattleId !==
|
||||
xuzhouStayBattlePayoffSourceBattleId ||
|
||||
value.targetBattleId !==
|
||||
xuzhouStayBattlePayoffTargetBattleId ||
|
||||
value.informationVisitId !==
|
||||
xuzhouStayInformationVisitId ||
|
||||
value.dialogueId !== xuzhouStayResonanceDialogueId ||
|
||||
typeof value.informationCompleted !== 'boolean' ||
|
||||
typeof value.miZhuDeployed !== 'boolean' ||
|
||||
(
|
||||
options.expectedBattleId !== undefined &&
|
||||
options.expectedBattleId !==
|
||||
xuzhouStayBattlePayoffTargetBattleId
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const choiceId =
|
||||
value.choiceId === undefined
|
||||
? undefined
|
||||
: isXuzhouStayBattlePayoffChoiceId(value.choiceId)
|
||||
? value.choiceId
|
||||
: undefined;
|
||||
if (value.choiceId !== undefined && !choiceId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (options.expectedMemory === null) {
|
||||
return undefined;
|
||||
}
|
||||
const expectedMemory = options.expectedMemory;
|
||||
if (
|
||||
expectedMemory &&
|
||||
(
|
||||
expectedMemory.recordId !== xuzhouStayBattlePayoffRecordId ||
|
||||
expectedMemory.sourceBattleId !==
|
||||
xuzhouStayBattlePayoffSourceBattleId ||
|
||||
expectedMemory.targetBattleId !==
|
||||
xuzhouStayBattlePayoffTargetBattleId ||
|
||||
expectedMemory.informationCompleted !==
|
||||
value.informationCompleted ||
|
||||
expectedMemory.choiceId !== choiceId
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rosterDeployment = expectedMiZhuDeployment(
|
||||
options.expectedRosterUnitIds
|
||||
);
|
||||
const miZhuDeployed =
|
||||
rosterDeployment ?? value.miZhuDeployed;
|
||||
const informationCounterplays = value.informationCompleted
|
||||
? cappedNonNegativeInteger(
|
||||
value.informationCounterplays,
|
||||
xuzhouStayInformationEnemyUnitIds.length
|
||||
)
|
||||
: 0;
|
||||
const supportEnabled = Boolean(choiceId && miZhuDeployed);
|
||||
const supportAttempts = supportEnabled
|
||||
? cappedNonNegativeInteger(
|
||||
value.supportAttempts,
|
||||
maxXuzhouStayBattlePayoffCounter
|
||||
)
|
||||
: 0;
|
||||
const requestedSupportUses = supportEnabled
|
||||
? Math.min(
|
||||
cappedNonNegativeInteger(value.supportUses, 1),
|
||||
supportAttempts
|
||||
)
|
||||
: 0;
|
||||
|
||||
const bonusHealing =
|
||||
choiceId === xuzhouStayShareStorehouseDutyChoiceId &&
|
||||
requestedSupportUses > 0
|
||||
? cappedNonNegativeInteger(
|
||||
value.bonusHealing,
|
||||
xuzhouStayBattlePayoffDefinition.dialogue.choices[
|
||||
xuzhouStayShareStorehouseDutyChoiceId
|
||||
].bonusHealing
|
||||
)
|
||||
: 0;
|
||||
const supportUses =
|
||||
choiceId === xuzhouStayShareStorehouseDutyChoiceId
|
||||
? bonusHealing > 0
|
||||
? requestedSupportUses
|
||||
: 0
|
||||
: requestedSupportUses;
|
||||
const bonusBuffTurns =
|
||||
choiceId === xuzhouStayTrustMiZhuLedgerChoiceId &&
|
||||
supportUses > 0
|
||||
? xuzhouStayBattlePayoffDefinition.dialogue.choices[
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
].bonusBuffTurns
|
||||
: 0;
|
||||
|
||||
return {
|
||||
version: xuzhouStayBattlePayoffVersion,
|
||||
recordId: xuzhouStayBattlePayoffRecordId,
|
||||
cityStayId: xuzhouStayId,
|
||||
sourceBattleId: xuzhouStayBattlePayoffSourceBattleId,
|
||||
targetBattleId: xuzhouStayBattlePayoffTargetBattleId,
|
||||
informationVisitId: xuzhouStayInformationVisitId,
|
||||
dialogueId: xuzhouStayResonanceDialogueId,
|
||||
informationCompleted: value.informationCompleted,
|
||||
...(choiceId ? { choiceId } : {}),
|
||||
informationCounterplays,
|
||||
miZhuDeployed,
|
||||
supportAttempts,
|
||||
supportUses,
|
||||
bonusHealing,
|
||||
bonusBuffTurns
|
||||
};
|
||||
}
|
||||
|
||||
function selectSourceVictoryRecord(
|
||||
campaign?: XuzhouStayBattlePayoffCampaignState | null
|
||||
): XuzhouStayBattlePayoffRecordSource | undefined {
|
||||
const history = campaign?.battleHistory;
|
||||
if (
|
||||
history &&
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
history,
|
||||
xuzhouStayBattlePayoffSourceBattleId
|
||||
)
|
||||
) {
|
||||
return isSourceVictoryRecord(
|
||||
history[xuzhouStayBattlePayoffSourceBattleId]
|
||||
)
|
||||
? 'battle-history'
|
||||
: undefined;
|
||||
}
|
||||
return isSourceVictoryRecord(campaign?.firstBattleReport)
|
||||
? 'legacy-report'
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function isSourceVictoryRecord(
|
||||
value?: XuzhouStayBattleRecord | null
|
||||
) {
|
||||
return Boolean(
|
||||
value &&
|
||||
value.battleId ===
|
||||
xuzhouStayBattlePayoffSourceBattleId &&
|
||||
value.outcome === 'victory'
|
||||
);
|
||||
}
|
||||
|
||||
function expectedMiZhuDeployment(
|
||||
value?: readonly unknown[] | ReadonlySet<string>
|
||||
) {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
return value.has(xuzhouStayMiZhuUnitId);
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
return value.some((unitId) => unitId === xuzhouStayMiZhuUnitId);
|
||||
}
|
||||
|
||||
function cappedNonNegativeInteger(value: unknown, max: number) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(max, Math.floor(numeric));
|
||||
}
|
||||
|
||||
function arrayValue(value: unknown): readonly unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
const maxXuzhouStayBattlePayoffCounter = 999999;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -381,6 +381,7 @@ export class CityStayScene extends Phaser.Scene {
|
||||
id: choice.id,
|
||||
label: choice.label,
|
||||
rewardExp: this.cityStay.dialogue.rewardExp + choice.rewardExp,
|
||||
battleEffect: choice.battleEffect ?? null,
|
||||
interactive: Boolean(view?.interactive),
|
||||
bounds: view ? this.boundsSnapshot(view.button.getBounds()) : null
|
||||
};
|
||||
@@ -1289,7 +1290,7 @@ export class CityStayScene extends Phaser.Scene {
|
||||
color: '#f1d691',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
const hint = this.add.text(150, 697, '유비의 답을 선택하세요. 선택은 저장되며 공명 경험치가 달라집니다.', {
|
||||
const hint = this.add.text(150, 697, '유비의 답을 선택하세요. 선택은 저장되며 공명과 다음 전투의 지원 방식이 달라집니다.', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '19px',
|
||||
color: '#aeb8c4'
|
||||
@@ -1318,7 +1319,7 @@ export class CityStayScene extends Phaser.Scene {
|
||||
});
|
||||
const reward = this.add.text(
|
||||
x + 76,
|
||||
y + 98,
|
||||
y + 126,
|
||||
`공명 +${dialogue.rewardExp + choice.rewardExp}`,
|
||||
{
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
@@ -1327,10 +1328,21 @@ export class CityStayScene extends Phaser.Scene {
|
||||
fontStyle: 'bold'
|
||||
}
|
||||
);
|
||||
const battleEffect = this.add.text(
|
||||
x + 76,
|
||||
y + 82,
|
||||
choice.battleEffect ?? '선택한 방침은 이후 대화와 공명에 반영됩니다.',
|
||||
{
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '17px',
|
||||
color: '#9fd7ef',
|
||||
wordWrap: { width: 670, useAdvancedWrap: true }
|
||||
}
|
||||
);
|
||||
button.on('pointerover', () => button.setFillStyle(0x2a3c4e, 0.99));
|
||||
button.on('pointerout', () => button.setFillStyle(0x1d2936, 0.98));
|
||||
button.on('pointerdown', () => this.chooseDialogue(choice));
|
||||
objects.push(button, number, label, reward);
|
||||
objects.push(button, number, label, battleEffect, reward);
|
||||
return { choiceId: choice.id, button, interactive: true };
|
||||
});
|
||||
const cancel = this.add.text(1740, 940, 'ESC · 나중에 결정', {
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
normalizeCityEquipmentContributionSnapshot,
|
||||
type CityEquipmentContributionSnapshot
|
||||
} from '../data/cityEquipmentPreparation';
|
||||
import {
|
||||
normalizeXuzhouStayBattlePayoffRuntime,
|
||||
type XuzhouStayBattlePayoffRuntime
|
||||
} from '../data/xuzhouStayBattlePayoff';
|
||||
|
||||
export type BattleSaveFaction = 'ally' | 'enemy';
|
||||
export type BattleSaveRosterTab = 'ally' | 'enemy';
|
||||
@@ -118,6 +122,8 @@ export type BattleSaveThirdCampPreparationState = {
|
||||
|
||||
export type BattleSaveCityEquipmentContributionState =
|
||||
CityEquipmentContributionSnapshot;
|
||||
export type BattleSaveXuzhouStayBattlePayoffState =
|
||||
XuzhouStayBattlePayoffRuntime;
|
||||
|
||||
export type BattleSaveEventPriority = 'critical' | 'high' | 'normal' | 'low';
|
||||
|
||||
@@ -141,6 +147,7 @@ export type BattleSaveState = {
|
||||
cooperationStatsByBond?: Record<string, BattleSaveCooperationStats>;
|
||||
thirdCampPreparation?: BattleSaveThirdCampPreparationState;
|
||||
cityEquipmentContribution?: BattleSaveCityEquipmentContributionState;
|
||||
xuzhouStayBattlePayoff?: BattleSaveXuzhouStayBattlePayoffState;
|
||||
savedAt: string;
|
||||
turnNumber: number;
|
||||
activeFaction: BattleSaveFaction;
|
||||
@@ -239,6 +246,7 @@ export function normalizeBattleSaveState(
|
||||
normalizeCooperationSaveFields(cloned, options);
|
||||
normalizeThirdCampPreparationSaveField(cloned);
|
||||
normalizeCityEquipmentContributionSaveField(cloned, options);
|
||||
normalizeXuzhouStayBattlePayoffSaveField(cloned, options);
|
||||
const recommendation = normalizeCampaignSortieRecommendationSnapshot(cloned.sortieRecommendation, {
|
||||
expectedBattleId: typeof cloned.battleId === 'string' ? cloned.battleId : options.expectedBattleId,
|
||||
allowedUnitIds: options.validAllyUnitIds
|
||||
@@ -296,6 +304,10 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isOptionalXuzhouStayBattlePayoffState(state.xuzhouStayBattlePayoff, state, options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attackIntents = state.attackIntents;
|
||||
const units = state.units;
|
||||
|
||||
@@ -803,6 +815,100 @@ function canonicalCityEquipmentContributionSaveState(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeXuzhouStayBattlePayoffSaveField(
|
||||
state: Record<string, unknown>,
|
||||
options: BattleSaveValidationOptions
|
||||
) {
|
||||
const normalized = canonicalXuzhouStayBattlePayoffSaveState(
|
||||
state.xuzhouStayBattlePayoff,
|
||||
state,
|
||||
options
|
||||
);
|
||||
if (normalized) {
|
||||
state.xuzhouStayBattlePayoff = normalized;
|
||||
} else {
|
||||
delete state.xuzhouStayBattlePayoff;
|
||||
}
|
||||
}
|
||||
|
||||
function isOptionalXuzhouStayBattlePayoffState(
|
||||
value: unknown,
|
||||
state: Record<string, unknown>,
|
||||
options: BattleSaveValidationOptions
|
||||
) {
|
||||
if (value === undefined) {
|
||||
return true;
|
||||
}
|
||||
const normalized = canonicalXuzhouStayBattlePayoffSaveState(
|
||||
value,
|
||||
state,
|
||||
options
|
||||
);
|
||||
if (!normalized || !isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
const fields: Array<
|
||||
keyof BattleSaveXuzhouStayBattlePayoffState
|
||||
> = [
|
||||
'version',
|
||||
'recordId',
|
||||
'cityStayId',
|
||||
'sourceBattleId',
|
||||
'targetBattleId',
|
||||
'informationVisitId',
|
||||
'dialogueId',
|
||||
'informationCompleted',
|
||||
'informationCounterplays',
|
||||
'miZhuDeployed',
|
||||
'supportAttempts',
|
||||
'supportUses',
|
||||
'bonusHealing',
|
||||
'bonusBuffTurns'
|
||||
];
|
||||
if (normalized.choiceId) {
|
||||
fields.push('choiceId');
|
||||
}
|
||||
return (
|
||||
Object.keys(value).length === fields.length &&
|
||||
fields.every((field) => value[field] === normalized[field])
|
||||
);
|
||||
}
|
||||
|
||||
function canonicalXuzhouStayBattlePayoffSaveState(
|
||||
value: unknown,
|
||||
state: Record<string, unknown>,
|
||||
options: BattleSaveValidationOptions
|
||||
): BattleSaveXuzhouStayBattlePayoffState | undefined {
|
||||
const battleId =
|
||||
typeof state.battleId === 'string' ? state.battleId : undefined;
|
||||
if (!battleId || !Array.isArray(state.units)) {
|
||||
return undefined;
|
||||
}
|
||||
const deployedUnitIds = state.units
|
||||
.filter(isRecord)
|
||||
.map((unit) => unit.id)
|
||||
.filter(
|
||||
(unitId): unitId is string =>
|
||||
typeof unitId === 'string' &&
|
||||
(!options.validAllyUnitIds ||
|
||||
options.validAllyUnitIds.has(unitId))
|
||||
);
|
||||
const normalized = normalizeXuzhouStayBattlePayoffRuntime(
|
||||
value,
|
||||
{
|
||||
expectedBattleId: battleId,
|
||||
expectedRosterUnitIds: deployedUnitIds
|
||||
}
|
||||
);
|
||||
if (
|
||||
!normalized ||
|
||||
(!normalized.informationCompleted && !normalized.choiceId)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isOptionalCoreResonanceState(
|
||||
state: Record<string, unknown>,
|
||||
options: BattleSaveValidationOptions
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
type CityEquipmentPreparationSnapshot,
|
||||
type CityEquipmentPurchaseCounts
|
||||
} from '../data/cityEquipmentPreparation';
|
||||
import {
|
||||
cloneXuzhouStayBattlePayoffSnapshot,
|
||||
normalizeXuzhouStayBattlePayoffSnapshot,
|
||||
resolveXuzhouStayBattlePayoff,
|
||||
type XuzhouStayBattlePayoffSnapshot
|
||||
} from '../data/xuzhouStayBattlePayoff';
|
||||
import { findCityStayAfterBattle, type CityStayId } from '../data/cityStays';
|
||||
import {
|
||||
isThirdCampPreparationPriorityId,
|
||||
@@ -190,6 +196,7 @@ export type FirstBattleReport = {
|
||||
sortieRecommendation?: CampaignSortieRecommendationSnapshot;
|
||||
sortieCooperation?: CampaignSortieCooperationSnapshot;
|
||||
cityEquipmentContribution?: CityEquipmentContributionSnapshot;
|
||||
xuzhouStayBattlePayoff?: XuzhouStayBattlePayoffSnapshot;
|
||||
completedCampDialogues: string[];
|
||||
completedCampVisits: string[];
|
||||
createdAt: string;
|
||||
@@ -475,6 +482,7 @@ export type CampaignBattleSettlement = {
|
||||
sortieRecommendation?: CampaignSortieRecommendationSnapshot;
|
||||
sortieCooperation?: CampaignSortieCooperationSnapshot;
|
||||
cityEquipmentContribution?: CityEquipmentContributionSnapshot;
|
||||
xuzhouStayBattlePayoff?: XuzhouStayBattlePayoffSnapshot;
|
||||
completedAt: string;
|
||||
};
|
||||
|
||||
@@ -1027,6 +1035,24 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
} else {
|
||||
delete reportClone.cityEquipmentContribution;
|
||||
}
|
||||
const xuzhouStayBattlePayoff =
|
||||
normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
reportClone.xuzhouStayBattlePayoff,
|
||||
{
|
||||
expectedBattleId: battleId,
|
||||
expectedRosterUnitIds: reportClone.units
|
||||
.filter((unit) => unit.faction === 'ally')
|
||||
.map((unit) => unit.id),
|
||||
expectedMemory:
|
||||
resolveXuzhouStayBattlePayoff(state, battleId) ?? null
|
||||
}
|
||||
);
|
||||
if (xuzhouStayBattlePayoff) {
|
||||
reportClone.xuzhouStayBattlePayoff =
|
||||
xuzhouStayBattlePayoff;
|
||||
} else {
|
||||
delete reportClone.xuzhouStayBattlePayoff;
|
||||
}
|
||||
const previousSettlement = state.battleHistory[battleId];
|
||||
const completedCampDialogues = mergeCampCompletionIds(
|
||||
state.completedCampDialogues,
|
||||
@@ -1899,6 +1925,7 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
|
||||
normalizeStoredCityEquipmentContributionsForContainers(
|
||||
normalized
|
||||
);
|
||||
normalizeStoredXuzhouStayBattlePayoffsForContainers(normalized);
|
||||
const recordedVictoryBattleIds = new Set(
|
||||
Object.entries(normalized.battleHistory)
|
||||
.filter(([, settlement]) => settlement.outcome === 'victory')
|
||||
@@ -2628,6 +2655,44 @@ function normalizeStoredCityEquipmentContributionsForContainers(
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStoredXuzhouStayBattlePayoffsForContainers(
|
||||
state: CampaignState
|
||||
) {
|
||||
if (state.firstBattleReport) {
|
||||
const payoff = normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
state.firstBattleReport.xuzhouStayBattlePayoff,
|
||||
{
|
||||
expectedBattleId: state.firstBattleReport.battleId,
|
||||
expectedRosterUnitIds: state.firstBattleReport.units
|
||||
.filter((unit) => unit.faction === 'ally')
|
||||
.map((unit) => unit.id)
|
||||
}
|
||||
);
|
||||
if (payoff) {
|
||||
state.firstBattleReport.xuzhouStayBattlePayoff = payoff;
|
||||
} else {
|
||||
delete state.firstBattleReport.xuzhouStayBattlePayoff;
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(state.battleHistory).forEach((settlement) => {
|
||||
const payoff = normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
settlement.xuzhouStayBattlePayoff,
|
||||
{
|
||||
expectedBattleId: settlement.battleId,
|
||||
expectedRosterUnitIds: settlement.units.map(
|
||||
(unit) => unit.unitId
|
||||
)
|
||||
}
|
||||
);
|
||||
if (payoff) {
|
||||
settlement.xuzhouStayBattlePayoff = payoff;
|
||||
} else {
|
||||
delete settlement.xuzhouStayBattlePayoff;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rosterUnitIds: Set<string>): FirstBattleReport {
|
||||
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(report.sortiePerformance, rosterUnitIds);
|
||||
const sortiePerformanceUnchanged = sortiePerformanceSnapshotsShareRoster(report.sortiePerformance, sortiePerformance);
|
||||
@@ -2793,6 +2858,14 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
|
||||
battleId,
|
||||
campaignSettlementContributionRoster(units)
|
||||
);
|
||||
const xuzhouStayBattlePayoff =
|
||||
normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
settlement.xuzhouStayBattlePayoff,
|
||||
{
|
||||
expectedBattleId: battleId,
|
||||
expectedRosterUnitIds: units.map((unit) => unit.unitId)
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
battleId,
|
||||
@@ -2814,6 +2887,9 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
|
||||
...(cityEquipmentContribution
|
||||
? { cityEquipmentContribution }
|
||||
: {}),
|
||||
...(xuzhouStayBattlePayoff
|
||||
? { xuzhouStayBattlePayoff }
|
||||
: {}),
|
||||
completedAt
|
||||
};
|
||||
}
|
||||
@@ -3171,6 +3247,16 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
|
||||
battleId,
|
||||
units
|
||||
);
|
||||
const xuzhouStayBattlePayoff =
|
||||
normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
report.xuzhouStayBattlePayoff,
|
||||
{
|
||||
expectedBattleId: battleId,
|
||||
expectedRosterUnitIds: units
|
||||
.filter((unit) => unit.faction === 'ally')
|
||||
.map((unit) => unit.id)
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
battleId,
|
||||
@@ -3197,6 +3283,9 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
|
||||
...(cityEquipmentContribution
|
||||
? { cityEquipmentContribution }
|
||||
: {}),
|
||||
...(xuzhouStayBattlePayoff
|
||||
? { xuzhouStayBattlePayoff }
|
||||
: {}),
|
||||
completedCampDialogues: uniqueCampHistoryIds(report.completedCampDialogues),
|
||||
completedCampVisits: uniqueCampHistoryIds(report.completedCampVisits),
|
||||
createdAt: normalizeCampaignTimestamp(report.createdAt) ?? new Date().toISOString()
|
||||
@@ -3472,6 +3561,14 @@ function createBattleSettlement(report: FirstBattleReport, reserveTraining: Camp
|
||||
)
|
||||
}
|
||||
: {}),
|
||||
...(report.xuzhouStayBattlePayoff
|
||||
? {
|
||||
xuzhouStayBattlePayoff:
|
||||
cloneXuzhouStayBattlePayoffSnapshot(
|
||||
report.xuzhouStayBattlePayoff
|
||||
)
|
||||
}
|
||||
: {}),
|
||||
completedAt: report.createdAt
|
||||
};
|
||||
}
|
||||
@@ -3641,6 +3738,12 @@ function cloneReport(report: FirstBattleReport): FirstBattleReport {
|
||||
cloned.cityEquipmentContribution
|
||||
);
|
||||
}
|
||||
if (cloned.xuzhouStayBattlePayoff) {
|
||||
cloned.xuzhouStayBattlePayoff =
|
||||
cloneXuzhouStayBattlePayoffSnapshot(
|
||||
cloned.xuzhouStayBattlePayoff
|
||||
);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user