feat: deepen audiovisual story immersion
This commit is contained in:
@@ -77,6 +77,8 @@ export const campaignVictoryRewardCategoryIds = [
|
||||
export type CampaignVictoryRewardCategoryId = (typeof campaignVictoryRewardCategoryIds)[number];
|
||||
export type CampaignVictoryRewardAcknowledgements = Partial<Record<string, CampaignVictoryRewardCategoryId[]>>;
|
||||
|
||||
export type CampaignCampChoiceHistory = Record<string, string>;
|
||||
|
||||
const campaignVictoryRewardCategoryIdSet = new Set<string>(campaignVictoryRewardCategoryIds);
|
||||
|
||||
export type CampaignSortieUnitPerformanceSnapshot = {
|
||||
@@ -373,6 +375,7 @@ const campaignInventoryCaps: Record<string, number> = {
|
||||
|
||||
const maxCampaignStringListEntries = 128;
|
||||
const maxCampaignStringListLength = 96;
|
||||
const maxCampaignCampHistoryEntries = 256;
|
||||
const maxCampaignUnknownInventoryAmount = 999;
|
||||
const maxCampaignRewardNoteLength = 180;
|
||||
const maxCampaignBattleObjectiveEntries = 24;
|
||||
@@ -485,6 +488,8 @@ export type CampaignState = {
|
||||
completedTutorialIds: CampaignTutorialId[];
|
||||
completedCampDialogues: string[];
|
||||
completedCampVisits: string[];
|
||||
campDialogueChoiceIds: CampaignCampChoiceHistory;
|
||||
campVisitChoiceIds: CampaignCampChoiceHistory;
|
||||
acknowledgedVictoryRewardBattleIds: string[];
|
||||
acknowledgedVictoryRewardCategories: CampaignVictoryRewardAcknowledgements;
|
||||
dismissedVictoryRewardNoticeBattleIds: string[];
|
||||
@@ -704,6 +709,8 @@ export function createInitialCampaignState(): CampaignState {
|
||||
completedTutorialIds: [],
|
||||
completedCampDialogues: [],
|
||||
completedCampVisits: [],
|
||||
campDialogueChoiceIds: {},
|
||||
campVisitChoiceIds: {},
|
||||
acknowledgedVictoryRewardBattleIds: [],
|
||||
acknowledgedVictoryRewardCategories: {},
|
||||
dismissedVictoryRewardNoticeBattleIds: [],
|
||||
@@ -918,10 +925,16 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
delete reportClone.sortieRecommendation;
|
||||
}
|
||||
const previousSettlement = state.battleHistory[battleId];
|
||||
const completedCampDialogues =
|
||||
battleId === 'first-battle-zhuo-commandery' ? [...reportClone.completedCampDialogues] : [...state.completedCampDialogues];
|
||||
const completedCampVisits =
|
||||
battleId === 'first-battle-zhuo-commandery' ? [...(reportClone.completedCampVisits ?? [])] : [...state.completedCampVisits];
|
||||
const completedCampDialogues = mergeCampCompletionIds(
|
||||
state.completedCampDialogues,
|
||||
reportClone.completedCampDialogues,
|
||||
Object.keys(state.campDialogueChoiceIds)
|
||||
);
|
||||
const completedCampVisits = mergeCampCompletionIds(
|
||||
state.completedCampVisits,
|
||||
reportClone.completedCampVisits ?? [],
|
||||
Object.keys(state.campVisitChoiceIds)
|
||||
);
|
||||
|
||||
reportClone.completedCampDialogues = completedCampDialogues;
|
||||
reportClone.completedCampVisits = completedCampVisits;
|
||||
@@ -1313,14 +1326,27 @@ function uniqueVictoryRewardCategories(value: unknown): CampaignVictoryRewardCat
|
||||
return campaignVictoryRewardCategoryIds.filter((category) => categories.has(category));
|
||||
}
|
||||
|
||||
export function applyCampBondExp(dialogueId: string, bondId: string, amount: number) {
|
||||
export function applyCampBondExp(dialogueId: string, bondId: string, amount: number, choiceId?: string) {
|
||||
const state = ensureCampaignState();
|
||||
if (!state.firstBattleReport) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hasCompletionFlag(state.completedCampDialogues, state.firstBattleReport.completedCampDialogues, dialogueId)) {
|
||||
if (syncCompletionFlag(state.completedCampDialogues, state.firstBattleReport.completedCampDialogues, dialogueId)) {
|
||||
if (
|
||||
hasCampCompletionFlag(
|
||||
state.completedCampDialogues,
|
||||
state.firstBattleReport.completedCampDialogues,
|
||||
state.campDialogueChoiceIds,
|
||||
dialogueId
|
||||
)
|
||||
) {
|
||||
const completionSynced = syncCompletionFlag(
|
||||
state.completedCampDialogues,
|
||||
state.firstBattleReport.completedCampDialogues,
|
||||
dialogueId
|
||||
);
|
||||
const choiceRemembered = rememberCampChoice(state.campDialogueChoiceIds, dialogueId, choiceId);
|
||||
if (completionSynced || choiceRemembered) {
|
||||
state.updatedAt = new Date().toISOString();
|
||||
persistCampaignState(state);
|
||||
}
|
||||
@@ -1334,6 +1360,7 @@ export function applyCampBondExp(dialogueId: string, bondId: string, amount: num
|
||||
}
|
||||
|
||||
syncCompletionFlag(state.completedCampDialogues, state.firstBattleReport.completedCampDialogues, dialogueId);
|
||||
rememberCampChoice(state.campDialogueChoiceIds, dialogueId, choiceId);
|
||||
advanceBond(campaignBond, amount);
|
||||
reportBond.level = campaignBond.level;
|
||||
reportBond.exp = campaignBond.exp;
|
||||
@@ -1346,15 +1373,29 @@ export function applyCampBondExp(dialogueId: string, bondId: string, amount: num
|
||||
|
||||
export function applyCampVisitReward(
|
||||
visitId: string,
|
||||
reward: { bondId?: string; bondExp?: number; gold?: number; itemRewards?: string[] }
|
||||
reward: { bondId?: string; bondExp?: number; gold?: number; itemRewards?: string[] },
|
||||
choiceId?: string
|
||||
) {
|
||||
const state = ensureCampaignState();
|
||||
if (!state.firstBattleReport) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hasCompletionFlag(state.completedCampVisits, state.firstBattleReport.completedCampVisits, visitId)) {
|
||||
if (syncCompletionFlag(state.completedCampVisits, state.firstBattleReport.completedCampVisits, visitId)) {
|
||||
if (
|
||||
hasCampCompletionFlag(
|
||||
state.completedCampVisits,
|
||||
state.firstBattleReport.completedCampVisits,
|
||||
state.campVisitChoiceIds,
|
||||
visitId
|
||||
)
|
||||
) {
|
||||
const completionSynced = syncCompletionFlag(
|
||||
state.completedCampVisits,
|
||||
state.firstBattleReport.completedCampVisits,
|
||||
visitId
|
||||
);
|
||||
const choiceRemembered = rememberCampChoice(state.campVisitChoiceIds, visitId, choiceId);
|
||||
if (completionSynced || choiceRemembered) {
|
||||
state.updatedAt = new Date().toISOString();
|
||||
persistCampaignState(state);
|
||||
}
|
||||
@@ -1362,6 +1403,7 @@ export function applyCampVisitReward(
|
||||
}
|
||||
|
||||
syncCompletionFlag(state.completedCampVisits, state.firstBattleReport.completedCampVisits, visitId);
|
||||
rememberCampChoice(state.campVisitChoiceIds, visitId, choiceId);
|
||||
|
||||
if (reward.bondId && reward.bondExp && reward.bondExp > 0) {
|
||||
const campaignBond = state.bonds.find((candidate) => candidate.id === reward.bondId);
|
||||
@@ -1389,8 +1431,31 @@ export function applyCampVisitReward(
|
||||
return cloneCampaignState(state);
|
||||
}
|
||||
|
||||
function hasCompletionFlag(stateFlags: string[], reportFlags: string[], flagId: string) {
|
||||
return stateFlags.includes(flagId) || reportFlags.includes(flagId);
|
||||
function hasCampCompletionFlag(
|
||||
stateFlags: string[],
|
||||
reportFlags: string[],
|
||||
choiceHistory: CampaignCampChoiceHistory,
|
||||
flagId: string
|
||||
) {
|
||||
const normalizedFlagId = normalizeKeyString(flagId);
|
||||
return (
|
||||
stateFlags.includes(flagId) ||
|
||||
reportFlags.includes(flagId) ||
|
||||
Boolean(normalizedFlagId && choiceHistory[normalizedFlagId])
|
||||
);
|
||||
}
|
||||
|
||||
function rememberCampChoice(history: CampaignCampChoiceHistory, entryId: string, choiceId?: string) {
|
||||
const normalizedEntryId = normalizeKeyString(entryId);
|
||||
const normalizedChoiceId = normalizeKeyString(choiceId);
|
||||
if (!normalizedEntryId || !normalizedChoiceId || history[normalizedEntryId]) {
|
||||
return false;
|
||||
}
|
||||
if (Object.keys(history).length >= maxCampaignCampHistoryEntries) {
|
||||
return false;
|
||||
}
|
||||
history[normalizedEntryId] = normalizedChoiceId;
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncCompletionFlag(stateFlags: string[], reportFlags: string[], flagId: string) {
|
||||
@@ -1566,8 +1631,10 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
|
||||
normalized.bonds = arrayOrEmpty<unknown>(normalized.bonds)
|
||||
.map(normalizeCampBondSnapshot)
|
||||
.filter((bond): bond is CampBondSnapshot => Boolean(bond));
|
||||
normalized.completedCampDialogues = uniqueStrings(normalized.completedCampDialogues);
|
||||
normalized.completedCampVisits = uniqueStrings(normalized.completedCampVisits);
|
||||
normalized.completedCampDialogues = uniqueCampHistoryIds(normalized.completedCampDialogues);
|
||||
normalized.completedCampVisits = uniqueCampHistoryIds(normalized.completedCampVisits);
|
||||
normalized.campDialogueChoiceIds = normalizeCampChoiceHistory(normalized.campDialogueChoiceIds);
|
||||
normalized.campVisitChoiceIds = normalizeCampChoiceHistory(normalized.campVisitChoiceIds);
|
||||
normalized.completedTutorialIds = uniqueStrings(normalized.completedTutorialIds)
|
||||
.filter((id): id is CampaignTutorialId => campaignTutorialIdSet.has(id));
|
||||
normalized.acknowledgedVictoryRewardBattleIds = uniqueStrings(normalized.acknowledgedVictoryRewardBattleIds)
|
||||
@@ -1580,6 +1647,20 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
|
||||
normalized.inventory = normalizeInventory(normalized.inventory);
|
||||
normalized.battleHistory = normalizeBattleHistory(normalized.battleHistory);
|
||||
normalized.firstBattleReport = normalizeFirstBattleReport(normalized.firstBattleReport);
|
||||
normalized.completedCampDialogues = mergeCampCompletionIds(
|
||||
normalized.completedCampDialogues,
|
||||
normalized.firstBattleReport?.completedCampDialogues ?? [],
|
||||
Object.keys(normalized.campDialogueChoiceIds)
|
||||
);
|
||||
normalized.completedCampVisits = mergeCampCompletionIds(
|
||||
normalized.completedCampVisits,
|
||||
normalized.firstBattleReport?.completedCampVisits ?? [],
|
||||
Object.keys(normalized.campVisitChoiceIds)
|
||||
);
|
||||
if (normalized.firstBattleReport) {
|
||||
normalized.firstBattleReport.completedCampDialogues = [...normalized.completedCampDialogues];
|
||||
normalized.firstBattleReport.completedCampVisits = [...normalized.completedCampVisits];
|
||||
}
|
||||
backfillEarlyJianYongRecruit(normalized);
|
||||
const rosterUnitIds = normalizedRosterUnitIds(normalized.roster);
|
||||
const sortieUnitFilter = rosterUnitIds.size > 0 ? rosterUnitIds : undefined;
|
||||
@@ -1734,6 +1815,40 @@ function normalizeCampaignVictoryRewardAcknowledgements(value: unknown): Campaig
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizeCampChoiceHistory(value: unknown): CampaignCampChoiceHistory {
|
||||
const history: CampaignCampChoiceHistory = {};
|
||||
let acceptedEntryCount = 0;
|
||||
for (const [entryId, choiceId] of Object.entries(recordOrEmpty(value))) {
|
||||
if (acceptedEntryCount >= maxCampaignCampHistoryEntries) {
|
||||
break;
|
||||
}
|
||||
const normalizedEntryId = normalizeKeyString(entryId);
|
||||
const normalizedChoiceId = normalizeKeyString(choiceId);
|
||||
if (normalizedEntryId && normalizedChoiceId) {
|
||||
if (!history[normalizedEntryId]) {
|
||||
acceptedEntryCount += 1;
|
||||
}
|
||||
history[normalizedEntryId] = normalizedChoiceId;
|
||||
}
|
||||
}
|
||||
return history;
|
||||
}
|
||||
|
||||
function uniqueCampHistoryIds(value: unknown): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
arrayOrEmpty<unknown>(value)
|
||||
.filter((entry): entry is string => typeof entry === 'string')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0 && entry.length <= maxCampaignStringListLength)
|
||||
)
|
||||
].slice(0, maxCampaignCampHistoryEntries);
|
||||
}
|
||||
|
||||
function mergeCampCompletionIds(...completionIdGroups: string[][]) {
|
||||
return uniqueCampHistoryIds(completionIdGroups.flat());
|
||||
}
|
||||
|
||||
function uniqueStrings(value: unknown): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
@@ -2704,8 +2819,8 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
|
||||
...(sortieReview ? { sortieReview } : {}),
|
||||
...(sortieOrder ? { sortieOrder } : {}),
|
||||
...(sortieRecommendation ? { sortieRecommendation } : {}),
|
||||
completedCampDialogues: uniqueStrings(report.completedCampDialogues),
|
||||
completedCampVisits: uniqueStrings(report.completedCampVisits),
|
||||
completedCampDialogues: uniqueCampHistoryIds(report.completedCampDialogues),
|
||||
completedCampVisits: uniqueCampHistoryIds(report.completedCampVisits),
|
||||
createdAt: normalizeCampaignTimestamp(report.createdAt) ?? new Date().toISOString()
|
||||
};
|
||||
}
|
||||
@@ -3102,8 +3217,8 @@ function cloneCampaignState(state: CampaignState): CampaignState {
|
||||
function cloneReport(report: FirstBattleReport): FirstBattleReport {
|
||||
const cloned = JSON.parse(JSON.stringify(report)) as FirstBattleReport;
|
||||
cloned.itemRewards = normalizeRewardStrings(cloned.itemRewards);
|
||||
cloned.completedCampDialogues = uniqueStrings(cloned.completedCampDialogues);
|
||||
cloned.completedCampVisits = uniqueStrings(cloned.completedCampVisits);
|
||||
cloned.completedCampDialogues = uniqueCampHistoryIds(cloned.completedCampDialogues);
|
||||
cloned.completedCampVisits = uniqueCampHistoryIds(cloned.completedCampVisits);
|
||||
if (cloned.campaignRewards) {
|
||||
cloned.campaignRewards = cloneCampaignRewardSnapshot(cloned.campaignRewards, cloned.battleId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user