diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 11249d1..7839fd8 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -975,7 +975,9 @@ export const sixthBattleScenario: BattleScenarioDefinition = { kind: 'secure-terrain', label: '계교 보급촌 확보', rewardGold: 300, - terrain: 'village' + terrain: 'village', + targetTile: { x: 17, y: 4, radius: 4 }, + threatRadius: 0 }, { id: 'quick', @@ -987,6 +989,14 @@ export const sixthBattleScenario: BattleScenarioDefinition = { ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], itemRewards: ['콩 2', '상처약 2', '탁주 1', '공손찬 신뢰 +1'], + campaignReward: { + supplies: ['콩 2', '상처약 2', '탁주 1'], + equipment: ['바람 화살통 1'], + reputation: ['공손찬 신뢰 +1'], + unlockBattleId: 'seventh-battle-xuzhou-rescue', + unlockLabel: '서주 구원전 개방', + note: '계교 원군로를 지켜 공손찬 진영의 신뢰를 얻고, 서주 구원 요청으로 이어집니다.' + }, victoryPages: sixthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -999,14 +1009,34 @@ export const seventhBattleScenario: BattleScenarioDefinition = { openingObjectiveLines: [ '조조군 선봉 하후돈이 서주 피난로와 마을을 압박하고 있습니다. 하후돈을 격파하면 서주 성문까지 길을 열 수 있습니다.', '강가 궁병은 자리를 지키고, 기병은 길을 따라 빠르게 추격합니다. 마을을 확보하며 선봉을 단계적으로 끊어 내십시오.', - '16턴 이내에 승리하면 도겸과 서주 백성의 신뢰가 크게 오릅니다.' + '18턴 이내에 승리하면 도겸과 서주 백성의 신뢰가 크게 오릅니다.' ], + sortie: { + sortieLimit: 3, + requiredUnits: ['liu-bei'], + recommendedUnits: [ + { unitId: 'liu-bei', role: 'support', reason: '서주 구원 요청의 중심입니다. 후방에서 생존과 보급 판단을 맡기면 전선이 안정됩니다.' }, + { unitId: 'guan-yu', role: 'front', reason: '마을 입구의 보병과 궁병을 받아내며 정면 전열을 세우기 좋습니다.' }, + { unitId: 'zhang-fei', role: 'flank', reason: '측면 길을 빠르게 압박해 기병 추격과 잔존 적 정리를 맡기 좋습니다.' } + ], + recommendedClasses: [ + { label: '마을 전열', classKeys: ['infantry'], reason: '마을 주변에서 방어 보정을 받으며 보병과 궁병을 안정적으로 상대합니다.' }, + { label: '측면 압박', classKeys: ['spearman'], reason: '길을 타고 들어오는 기병과 측면 적을 끊어내는 역할이 중요합니다.' }, + { label: '후방 지휘', classKeys: ['lord'], reason: '유비가 무리하지 않고 생존하면 서주 민심과 합류 보상이 자연스럽게 이어집니다.' } + ], + deploymentSlots: [ + { role: 'front', unitId: 'guan-yu', x: 3, y: 16, label: '마을 전열' }, + { role: 'flank', unitId: 'zhang-fei', x: 2, y: 17, label: '측면 압박' }, + { role: 'support', unitId: 'liu-bei', x: 2, y: 16, label: '후방 지휘' } + ], + note: '서주 구원전은 마을 확보와 유비 생존이 핵심입니다. 기본 전열은 관우 정면, 장비 측면, 유비 후방 지휘로 잡혀 있습니다.' + }, map: seventhBattleMap, units: seventhBattleUnits, bonds: seventhBattleBonds, mapTextureKey: 'battle-map-seventh', leaderUnitId: 'xuzhou-leader-xiahou-dun', - quickVictoryTurnLimit: 16, + quickVictoryTurnLimit: 18, baseVictoryGold: 960, objectives: [ { @@ -1028,18 +1058,29 @@ export const seventhBattleScenario: BattleScenarioDefinition = { kind: 'secure-terrain', label: '서주 피난촌 확보', rewardGold: 330, - terrain: 'village' + terrain: 'village', + targetTile: { x: 16, y: 4, radius: 5 }, + threatRadius: 0 }, { id: 'quick', kind: 'quick-victory', - label: '16턴 이내 승리', + label: '18턴 이내 승리', rewardGold: 270, - maxTurn: 16 + maxTurn: 18 } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], itemRewards: ['콩 2', '상처약 2', '탁주 1', '서주 민심 +1'], + campaignReward: { + supplies: ['콩 2', '상처약 2', '탁주 1'], + equipment: ['병법서 1'], + reputation: ['서주 민심 +1'], + recruits: ['jian-yong', 'mi-zhu'], + unlockBattleId: 'eighth-battle-xiaopei-supply-road', + unlockLabel: '소패 보급로 방위전 개방', + note: '서주 구원으로 간옹과 미축이 합류합니다. 다음 전투부터 책사/보급관 선택지가 출전 준비에 표시됩니다.' + }, victoryPages: seventhBattleVictoryPages, nextCampScene: 'CampScene' }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 1f18994..9c3d224 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -7375,6 +7375,27 @@ export const jiangWeiRecruitUnits: UnitData[] = [ } ]; +export const campaignRecruitUnits: UnitData[] = [ + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits, + ...luoCastleProperRecruitUnits, + ...chengduPressureRecruitUnits, + ...chengduSurrenderRecruitUnits, + ...hanzhongOpeningRecruitUnits, + ...hanzhongScoutRecruitUnits, + ...hanzhongMainRecruitUnits, + ...jiangWeiRecruitUnits +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 11d7ba5..1044b83 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -1,5 +1,5 @@ import { equipmentExpToNext, equipmentSlots } from '../data/battleItems'; -import type { BattleBond, UnitData } from '../data/scenario'; +import { campaignRecruitUnits, type BattleBond, type UnitData } from '../data/scenario'; import { normalizeSortieFormationAssignments, type SortieFormationAssignments } from '../data/sortieDeployment'; export type BattleObjectiveSnapshot = { @@ -397,6 +397,8 @@ export type CampaignSaveSlotSummary = { let campaignState: CampaignState | undefined; +const campaignRecruitUnitById = new Map(campaignRecruitUnits.map((unit) => [unit.id, unit])); + export function createInitialCampaignState(): CampaignState { return { version: 1, @@ -522,7 +524,10 @@ export function setFirstBattleReport(report: FirstBattleReport) { state.gold = Math.max(0, state.gold - (previousSettlement?.rewardGold ?? 0) + reportClone.rewardGold); const alliedReportUnits = reportClone.units.filter((unit) => unit.faction === 'ally'); const recruitUnitIds = new Set(reportClone.campaignRewards?.recruits.map((recruit) => recruit.unitId) ?? []); - const recruitedUnits = reportClone.units.filter((unit) => unit.faction === 'ally' && recruitUnitIds.has(unit.id)); + const reportUnitById = new Map(reportClone.units.filter((unit) => unit.faction === 'ally').map((unit) => [unit.id, unit])); + const recruitedUnits = [...recruitUnitIds] + .map((unitId) => reportUnitById.get(unitId) ?? campaignRecruitUnitById.get(unitId)) + .filter((unit): unit is UnitData => Boolean(unit)); state.roster = mergeRosterProgress(state.roster, [...alliedReportUnits, ...recruitedUnits]); state.bonds = reportClone.bonds.map(cloneBondSnapshot); const reserveTraining = previousSettlement || reportClone.outcome !== 'victory'