From 70cd7305a89eceda47fc0f53bf9d93240983a667 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 13:18:07 +0900 Subject: [PATCH] Cap max bond experience gains --- .../verify-campaign-save-normalization.mjs | 39 +++++++++++++++++++ src/game/scenes/BattleScene.ts | 33 +++++++++------- src/game/scenes/CampScene.ts | 19 +++++---- src/game/state/campaignState.ts | 25 +++++++----- 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index 9c15350..0c32671 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -206,6 +206,45 @@ try { dialogueResynced.firstBattleReport?.completedCampDialogues.includes('repeat-dialogue'), `Expected report-only camp dialogue completion to resync without duplicate bond exp: ${JSON.stringify(dialogueResynced)}` ); + const maxedBondState = getCampaignState(); + const maxedCampaignBond = maxedBondState.bonds.find((bond) => bond.id === repeatBondId); + const maxedReportBond = maxedBondState.firstBattleReport?.bonds.find((bond) => bond.id === repeatBondId); + assert(maxedCampaignBond && maxedReportBond, `Expected repeat bond to exist before max-level checks: ${JSON.stringify(maxedBondState)}`); + maxedCampaignBond.level = 100; + maxedCampaignBond.exp = 95; + maxedCampaignBond.battleExp = 3; + maxedReportBond.level = 100; + maxedReportBond.exp = 95; + maxedReportBond.battleExp = 3; + maxedBondState.completedCampDialogues = maxedBondState.completedCampDialogues.filter((id) => id !== 'maxed-dialogue'); + maxedBondState.firstBattleReport.completedCampDialogues = maxedBondState.firstBattleReport.completedCampDialogues.filter((id) => id !== 'maxed-dialogue'); + maxedBondState.completedCampVisits = maxedBondState.completedCampVisits.filter((id) => id !== 'maxed-visit'); + maxedBondState.firstBattleReport.completedCampVisits = maxedBondState.firstBattleReport.completedCampVisits.filter((id) => id !== 'maxed-visit'); + setCampaignState(maxedBondState); + applyCampBondExp('maxed-dialogue', repeatBondId, 10); + const maxedDialogueState = getCampaignState(); + const maxedDialogueBond = maxedDialogueState.bonds.find((bond) => bond.id === repeatBondId); + const maxedDialogueReportBond = maxedDialogueState.firstBattleReport?.bonds.find((bond) => bond.id === repeatBondId); + assert( + maxedDialogueBond?.level === 100 && + maxedDialogueBond.exp === 100 && + maxedDialogueBond.battleExp === 13 && + maxedDialogueReportBond?.exp === 100 && + maxedDialogueReportBond.battleExp === 13, + `Expected max-level dialogue bond exp to cap instead of wrapping: ${JSON.stringify(maxedDialogueState)}` + ); + applyCampVisitReward('maxed-visit', { bondId: repeatBondId, bondExp: 10 }); + const maxedVisitState = getCampaignState(); + const maxedVisitBond = maxedVisitState.bonds.find((bond) => bond.id === repeatBondId); + const maxedVisitReportBond = maxedVisitState.firstBattleReport?.bonds.find((bond) => bond.id === repeatBondId); + assert( + maxedVisitBond?.level === 100 && + maxedVisitBond.exp === 100 && + maxedVisitBond.battleExp === 23 && + maxedVisitReportBond?.exp === 100 && + maxedVisitReportBond.battleExp === 23, + `Expected max-level visit bond exp to cap instead of wrapping: ${JSON.stringify(maxedVisitState)}` + ); storage.clear(); storage.set( diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 44b4c24..d200053 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -14481,14 +14481,7 @@ export class BattleScene extends Phaser.Scene { } const previousLevel = bond.level; const previousExp = bond.exp; - let leveled = false; - bond.battleExp += 4; - bond.exp += 4; - while (bond.exp >= 100) { - bond.exp -= 100; - bond.level = Math.min(100, bond.level + 1); - leveled = true; - } + const leveled = this.advanceBattleBond(bond, 4); const [first, second] = bond.unitIds.map((unitId) => this.unitName(unitId)); growthResults.push({ label: `${first}·${second}`, @@ -14524,12 +14517,7 @@ export class BattleScene extends Phaser.Scene { if (!bond) { return; } - bond.battleExp += 4; - bond.exp += 4; - while (bond.exp >= 100) { - bond.exp -= 100; - bond.level = Math.min(100, bond.level + 1); - } + this.advanceBattleBond(bond, 4); gained += 4; }); @@ -14541,6 +14529,23 @@ export class BattleScene extends Phaser.Scene { return `${unit.name} 공격 명령 완료\n예상 목표: ${target.name}\n${this.formatEquipmentGrowth(weaponGrowth)}${chain}${exp}`; } + private advanceBattleBond(bond: BondState, amount: number) { + const gained = Math.max(0, Math.floor(amount)); + if (gained <= 0) { + return false; + } + + const previousLevel = bond.level; + bond.battleExp += gained; + let exp = bond.exp + gained; + while (bond.level < 100 && exp >= 100) { + exp -= 100; + bond.level += 1; + } + bond.exp = bond.level >= 100 ? Math.min(exp, 100) : exp; + return bond.level > previousLevel; + } + private resolveEnemyPressure() { const defender = this.findEnemyPressureTarget(); if (!defender) { diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index 677ceaf..5b80332 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -14786,12 +14786,18 @@ export class CampScene extends Phaser.Scene { } private advanceCampBond(bond: CampaignState['bonds'][number], amount: number) { - bond.battleExp += amount; - bond.exp += amount; - while (bond.exp >= 100) { - bond.exp -= 100; - bond.level = Math.min(100, bond.level + 1); + const gained = Math.max(0, Math.floor(amount)); + if (gained <= 0) { + return; } + + bond.battleExp += gained; + let exp = bond.exp + gained; + while (bond.level < 100 && exp >= 100) { + exp -= 100; + bond.level += 1; + } + bond.exp = bond.level >= 100 ? Math.min(exp, 100) : exp; } private syncReportUnitHp(campaign: CampaignState, unitId: string, hp: number) { @@ -14963,8 +14969,7 @@ export class CampScene extends Phaser.Scene { this.report.completedCampDialogues.push(dialogue.id); const bond = this.report.bonds.find((candidate) => candidate.id === dialogue.bondId); if (bond) { - bond.battleExp += rewardExp; - bond.exp += rewardExp; + this.advanceCampBond(bond, rewardExp); } } diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 893c81e..d486130 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -961,8 +961,9 @@ function normalizeBondLevel(value: unknown) { return Math.min(100, normalizeNonNegativeInteger(value)); } -function normalizeBondExp(value: unknown) { - return Math.min(99, normalizeNonNegativeInteger(value)); +function normalizeBondExpForLevel(level: number, value: unknown) { + const exp = normalizeNonNegativeInteger(value); + return Math.min(exp, level >= 100 ? 100 : 99); } function normalizeCampaignTimestamp(value: unknown) { @@ -1164,7 +1165,7 @@ function normalizeCampaignBondProgressSnapshot(value: unknown): CampaignBondProg id, title, level, - exp: normalizeBondExp(value.exp), + exp: normalizeBondExpForLevel(level, value.exp), battleExp: normalizeNonNegativeInteger(value.battleExp) }; } @@ -1391,7 +1392,7 @@ function normalizeCampBondSnapshot(value: unknown): CampBondSnapshot | undefined title, unitIds: [unitIds[0], unitIds[1]] as [string, string], level, - exp: normalizeBondExp(value.exp), + exp: normalizeBondExpForLevel(level, value.exp), battleExp: normalizeNonNegativeInteger(value.battleExp) }; } @@ -1466,12 +1467,18 @@ function readLatestSlottedCampaignState() { } function advanceBond(bond: CampBondSnapshot, amount: number) { - bond.battleExp += amount; - bond.exp += amount; - while (bond.exp >= 100) { - bond.exp -= 100; - bond.level = Math.min(100, bond.level + 1); + const gained = normalizeNonNegativeInteger(amount); + if (gained <= 0) { + return; } + + bond.battleExp += gained; + let exp = bond.exp + gained; + while (bond.level < 100 && exp >= 100) { + exp -= 100; + bond.level += 1; + } + bond.exp = bond.level >= 100 ? Math.min(exp, 100) : exp; } function createBattleSettlement(report: FirstBattleReport, reserveTraining: CampaignReserveTrainingSnapshot[] = []): CampaignBattleSettlement {