diff --git a/scripts/qa-representative-battles.mjs b/scripts/qa-representative-battles.mjs index 642a7fc..5bbffd6 100644 --- a/scripts/qa-representative-battles.mjs +++ b/scripts/qa-representative-battles.mjs @@ -1,10 +1,22 @@ import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { readFile, writeFile } from 'node:fs/promises'; import { chromium } from 'playwright'; const targetUrl = process.env.QA_URL ?? 'http://localhost:5173/'; const headless = process.env.QA_HEADLESS !== '0'; const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80); const pnpmCommand = process.env.PNPM_CMD ?? 'pnpm'; +const cumulativeMode = process.env.QA_CUMULATIVE === '1'; +const campaignStorageKey = 'heros-web:campaign-state'; +const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1'; +const campaignSnapshotPath = process.env.QA_CAMPAIGN_SNAPSHOT_PATH; +const resumeCampaignSnapshot = process.env.QA_RESUME_CAMPAIGN_SNAPSHOT === '1'; +const supplyLabels = { + bean: '\uCF69', + salve: '\uC0C1\uCC98\uC57D', + wine: '\uD0C1\uC8FC' +}; const coreBrothers = ['liu-bei', 'guan-yu', 'zhang-fei']; const xuzhouFullSortie = ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']; @@ -47,6 +59,18 @@ const mengHuoFourthSortie = ['zhuge-liang', 'ma-liang', 'huang-quan', 'wang-ping const mengHuoFifthSortie = ['zhuge-liang', 'huang-quan', 'ma-liang', 'wang-ping', 'zhao-yun', 'huang-zhong', 'ma-dai']; const mengHuoSixthSortie = ['zhuge-liang', 'ma-liang', 'huang-quan', 'wang-ping', 'zhao-yun', 'wei-yan', 'ma-dai']; const mengHuoFinalSortie = ['zhuge-liang', 'huang-quan', 'ma-liang', 'wang-ping', 'zhao-yun', 'ma-dai', 'huang-zhong']; +const northernFirstSortie = ['zhuge-liang', 'zhao-yun', 'huang-quan', 'ma-liang', 'wang-ping', 'ma-dai', 'wei-yan']; +const northernSecondSortie = ['zhuge-liang', 'zhao-yun', 'huang-quan', 'ma-liang', 'wang-ping', 'ma-dai', 'wei-yan']; +const northernThirdSortie = ['zhuge-liang', 'wang-ping', 'zhao-yun', 'huang-quan', 'ma-liang', 'ma-dai', 'wei-yan']; +const northernFourthSortie = ['zhuge-liang', 'jiang-wei', 'wang-ping', 'zhao-yun', 'ma-dai', 'huang-quan', 'ma-liang']; +const northernFifthSortie = ['zhuge-liang', 'jiang-wei', 'wei-yan', 'wang-ping', 'ma-dai', 'huang-quan', 'fa-zheng']; +const northernSixthSortie = ['zhuge-liang', 'jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang']; +const northernSeventhSortie = ['zhuge-liang', 'jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'ma-liang', 'wei-yan']; +const northernEighthSortie = ['zhuge-liang', 'jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang']; +const northernNinthSortie = ['zhuge-liang', 'jiang-wei', 'zhao-yun', 'wei-yan', 'wang-ping', 'huang-quan', 'li-yan']; +const northernTenthSortie = ['zhuge-liang', 'jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'wei-yan', 'ma-dai']; +const northernEleventhSortie = ['zhuge-liang', 'jiang-wei', 'ma-dai', 'wang-ping', 'huang-quan', 'li-yan', 'wei-yan']; +const northernFinalSortie = ['zhuge-liang', 'jiang-wei', 'wang-ping', 'ma-dai', 'huang-quan', 'li-yan', 'wei-yan']; const earlyCampaignBattles = [ { no: 1, id: 'first-battle-zhuo-commandery', selected: [], targets: ['rebel-leader'], protected: ['liu-bei'] }, @@ -438,6 +462,103 @@ const nanzhongFinalCampaignBattles = [ } ]; +const northernCampaignBattles = [ + { + no: 55, + id: 'fifty-fifth-battle-northern-qishan-road', + selected: northernFirstSortie, + targets: ['northern-first-leader-cao-zhen'], + protected: ['zhuge-liang'] + }, + { + no: 56, + id: 'fifty-sixth-battle-tianshui-advance', + selected: northernSecondSortie, + targets: ['northern-second-officer-jiang-wei', 'northern-second-leader-guo-huai'], + protected: ['zhuge-liang'] + }, + { + no: 57, + id: 'fifty-seventh-battle-jieting-crisis', + selected: northernThirdSortie, + targets: ['northern-third-officer-jiang-wei', 'northern-third-leader-zhang-he'], + protected: ['zhuge-liang', 'wang-ping'] + }, + { + no: 58, + id: 'fifty-eighth-battle-qishan-retreat', + selected: northernFourthSortie, + targets: ['northern-fourth-leader-sima-yi', 'northern-fourth-officer-zhang-he'], + protected: ['zhuge-liang'] + }, + { + no: 59, + id: 'fifty-ninth-battle-chencang-siege', + selected: northernFifthSortie, + targets: ['northern-fifth-leader-hao-zhao', 'northern-fifth-officer-wang-shuang'], + protected: ['zhuge-liang'] + }, + { + no: 60, + id: 'sixtieth-battle-wudu-yinping', + selected: northernSixthSortie, + targets: ['northern-sixth-leader-guo-huai', 'northern-sixth-officer-wudu', 'northern-sixth-officer-yinping'], + protected: ['zhuge-liang'] + }, + { + no: 61, + id: 'sixty-first-battle-hanzhong-rain-defense', + selected: northernSeventhSortie, + targets: ['northern-seventh-leader-cao-zhen', 'northern-seventh-officer-sima-yi', 'northern-seventh-officer-supply'], + protected: ['zhuge-liang'] + }, + { + no: 62, + id: 'sixty-second-battle-qishan-renewed-offensive', + selected: northernEighthSortie, + targets: ['northern-eighth-leader-sima-yi', 'northern-eighth-officer-zhang-he', 'northern-eighth-officer-guo-huai'], + protected: ['zhuge-liang'] + }, + { + no: 63, + id: 'sixty-third-battle-lucheng-pursuit', + selected: northernNinthSortie, + targets: ['northern-ninth-leader-sima-yi', 'northern-ninth-officer-zhang-he', 'northern-ninth-officer-guo-huai'], + protected: ['zhuge-liang'] + }, + { + no: 64, + id: 'sixty-fourth-battle-weishui-camps', + selected: northernTenthSortie, + targets: ['northern-tenth-leader-sima-yi', 'northern-tenth-officer-zhang-he', 'northern-tenth-officer-guo-huai'], + protected: ['zhuge-liang'] + }, + { + no: 65, + id: 'sixty-fifth-battle-weishui-northbank', + selected: northernEleventhSortie, + targets: ['northern-eleventh-officer-zhang-he', 'northern-eleventh-leader-sima-yi', 'northern-eleventh-officer-guo-huai'], + protected: ['zhuge-liang'] + }, + { + no: 66, + id: 'sixty-sixth-battle-wuzhang-final', + selected: northernFinalSortie, + targets: ['northern-twelfth-officer-zhang-he', 'northern-twelfth-leader-sima-yi', 'northern-twelfth-officer-guo-huai'], + protected: ['zhuge-liang'] + } +]; + +const allCampaignBattles = [ + ...earlyCampaignBattles, + ...refugeCampaignBattles, + ...southernCampaignBattles, + ...hanzhongCampaignBattles, + ...yilingCampaignBattles, + ...nanzhongFinalCampaignBattles, + ...northernCampaignBattles +]; + const allRepresentativeBattles = [ { no: 1, id: 'first-battle-zhuo-commandery', selected: [], targets: ['rebel-leader'], protected: ['liu-bei'] }, { @@ -500,12 +621,14 @@ const allRepresentativeBattles = [ const qaBattleSets = { representative: allRepresentativeBattles, + campaign: allCampaignBattles, early: earlyCampaignBattles, refuge: refugeCampaignBattles, southern: southernCampaignBattles, hanzhong: hanzhongCampaignBattles, yiling: yilingCampaignBattles, - nanzhongFinal: nanzhongFinalCampaignBattles + nanzhongFinal: nanzhongFinalCampaignBattles, + northern: northernCampaignBattles }; const qaSetName = process.env.QA_SET ?? 'representative'; const qaBattleSet = qaBattleSets[qaSetName]; @@ -536,12 +659,27 @@ try { for (const battle of representativeBattles) { console.log(`QA battle ${battle.no}: ${battle.id}`); - await setupBattle(page, battle); + const preparation = cumulativeMode ? await setupCumulativeBattle(page, battle, results.length === 0) : await setupBattle(page, battle); const result = await playBattleWithoutDebugVictory(page, battle); + if (preparation) { + result.campSuppliesUsed = preparation.suppliesUsed; + } + if (cumulativeMode) { + result.campaign = await readCampaignSummary(page); + if (campaignSnapshotPath) { + await writeCampaignSnapshot(page, campaignSnapshotPath); + } + } results.push(result); + const stockLine = result.campaign + ? ` stock=${result.campaign.supplies.bean}/${result.campaign.supplies.salve}/${result.campaign.supplies.wine} gold=${result.campaign.gold} avgLv=${result.campaign.levelAvg}` + : ''; console.log( - `QA result ${result.no}: ${result.outcome} turn=${result.finalTurn} allies=${result.alliesAlive} enemies=${result.enemiesAlive} items=${result.itemsUsed} objectives=${result.objectivesAchieved}/${result.objectiveCount}` + `QA result ${result.no}: ${result.outcome} turn=${result.finalTurn} allies=${result.alliesAlive} enemies=${result.enemiesAlive} items=${result.itemsUsed} objectives=${result.objectivesAchieved}/${result.objectiveCount}${stockLine}` ); + if (result.outcome !== 'victory') { + break; + } } const failed = results.filter((result) => result.outcome !== 'victory'); @@ -553,11 +691,35 @@ try { alliesAlive: result.alliesAlive, enemiesAlive: result.enemiesAlive, itemsUsed: result.itemsUsed, + campUsed: result.campSuppliesUsed + ? `${result.campSuppliesUsed.bean}/${result.campSuppliesUsed.salve}/${result.campSuppliesUsed.wine}` + : '', + stock: result.campaign ? `${result.campaign.supplies.bean}/${result.campaign.supplies.salve}/${result.campaign.supplies.wine}` : '', + level: result.campaign ? `${result.campaign.levelMin}-${result.campaign.levelMax} (${result.campaign.levelAvg})` : '', + lowHp: result.campaign?.lowHp ?? '', supports: result.supports, objectives: `${result.objectivesAchieved}/${result.objectiveCount}` })) ); + if (cumulativeMode && results.length > 0) { + const latest = results.at(-1).campaign; + console.dir( + { + mode: 'cumulative', + battleCount: latest?.battleCount, + gold: latest?.gold, + supplies: latest?.supplies, + rosterCount: latest?.rosterCount, + levelRange: latest ? `${latest.levelMin}-${latest.levelMax}` : undefined, + levelAvg: latest?.levelAvg, + lowHp: latest?.lowHp, + topLevels: latest?.topLevels + }, + { depth: null } + ); + } + if (failed.length > 0) { console.dir( failed.map((result) => ({ @@ -622,6 +784,189 @@ async function setupBattle(page, battle) { ); } +async function setupCumulativeBattle(page, battle, firstBattle) { + const initialSnapshot = + firstBattle && resumeCampaignSnapshot && campaignSnapshotPath && existsSync(campaignSnapshotPath) + ? await readFile(campaignSnapshotPath, 'utf8') + : undefined; + await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined && window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 }); + const preparation = await page.evaluate( + ({ firstBattle, initialSnapshot, labels, protectedUnitIds, selected, slotKey, storageKey }) => { + const now = new Date().toISOString(); + const defaultState = { + version: 1, + updatedAt: now, + step: 'first-battle', + activeSaveSlot: 1, + gold: 0, + roster: [], + bonds: [], + inventory: {}, + selectedSortieUnitIds: selected, + reserveTrainingFocus: 'balanced', + completedCampDialogues: [], + completedCampVisits: [], + battleHistory: {} + }; + + if (firstBattle && !initialSnapshot) { + window.localStorage.clear(); + } + + const rawState = initialSnapshot ?? (firstBattle ? undefined : window.localStorage.getItem(slotKey) ?? window.localStorage.getItem(storageKey)); + const state = rawState ? JSON.parse(rawState) : defaultState; + const suppliesUsed = { bean: 0, salve: 0, wine: 0 }; + state.updatedAt = now; + state.activeSaveSlot = 1; + state.inventory = state.inventory ?? {}; + state.roster = Array.isArray(state.roster) ? state.roster : []; + state.selectedSortieUnitIds = [...new Set(selected)]; + state.reserveTrainingFocus = state.reserveTrainingFocus ?? 'balanced'; + + const selectedIds = new Set(selected); + const protectedIds = new Set(protectedUnitIds); + const healAmounts = { + [labels.bean]: 12, + [labels.salve]: 22, + [labels.wine]: 8 + }; + + const consume = (label) => { + if ((state.inventory[label] ?? 0) <= 0) { + return false; + } + state.inventory[label] -= 1; + if (state.inventory[label] <= 0) { + delete state.inventory[label]; + } + return true; + }; + + const applySupply = (unit, supplyId, label) => { + if (!consume(label)) { + return false; + } + unit.hp = Math.min(unit.maxHp, unit.hp + healAmounts[label]); + suppliesUsed[supplyId] += 1; + return true; + }; + + for (const unit of state.roster) { + if (!selectedIds.has(unit.id) && !protectedIds.has(unit.id)) { + continue; + } + if (typeof unit.hp !== 'number' || typeof unit.maxHp !== 'number' || unit.maxHp <= 0) { + continue; + } + + const targetRatio = protectedIds.has(unit.id) ? 1 : 0.86; + const targetHp = Math.min(unit.maxHp, Math.ceil(unit.maxHp * targetRatio)); + while (unit.hp < targetHp) { + const missing = targetHp - unit.hp; + const ratio = unit.hp / unit.maxHp; + if (missing >= 17 && (state.inventory[labels.salve] ?? 0) > 0) { + applySupply(unit, 'salve', labels.salve); + } else if (missing >= 9 && (state.inventory[labels.bean] ?? 0) > 0) { + applySupply(unit, 'bean', labels.bean); + } else if (ratio < 0.7 && missing >= 5 && (state.inventory[labels.wine] ?? 0) > 0) { + applySupply(unit, 'wine', labels.wine); + } else if (protectedIds.has(unit.id) && (state.inventory[labels.bean] ?? 0) > 0) { + applySupply(unit, 'bean', labels.bean); + } else { + break; + } + } + } + + window.localStorage.setItem(storageKey, JSON.stringify(state)); + window.localStorage.setItem(slotKey, JSON.stringify(state)); + return { + suppliesUsed, + stock: { + bean: state.inventory[labels.bean] ?? 0, + salve: state.inventory[labels.salve] ?? 0, + wine: state.inventory[labels.wine] ?? 0 + } + }; + }, + { + firstBattle, + initialSnapshot, + labels: supplyLabels, + protectedUnitIds: battle.protected, + selected: battle.selected, + slotKey: campaignSlotStorageKey, + storageKey: campaignStorageKey + } + ); + + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined && window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 }); + await page.evaluate((battleId) => window.__HEROS_DEBUG__.goToBattle(battleId), battle.id); + await page.waitForFunction( + (battleId) => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === battleId && state?.phase === 'idle' && state?.mapBackgroundReady === true; + }, + battle.id, + { timeout: 90000 } + ); + return preparation; +} + +async function writeCampaignSnapshot(page, snapshotPath) { + const snapshot = await page.evaluate( + ({ slotKey, storageKey }) => window.localStorage.getItem(slotKey) ?? window.localStorage.getItem(storageKey), + { slotKey: campaignSlotStorageKey, storageKey: campaignStorageKey } + ); + if (snapshot) { + await writeFile(snapshotPath, snapshot, 'utf8'); + } +} + +async function readCampaignSummary(page) { + return page.evaluate( + ({ labels, slotKey, storageKey }) => { + const rawState = window.localStorage.getItem(slotKey) ?? window.localStorage.getItem(storageKey); + if (!rawState) { + return undefined; + } + const state = JSON.parse(rawState); + const allies = (state.roster ?? []).filter((unit) => unit.faction === 'ally'); + const levels = allies.map((unit) => unit.level ?? 1); + const hpRatios = allies.map((unit) => { + const maxHp = typeof unit.maxHp === 'number' && unit.maxHp > 0 ? unit.maxHp : 1; + return (unit.hp ?? maxHp) / maxHp; + }); + const levelMin = levels.length > 0 ? Math.min(...levels) : 0; + const levelMax = levels.length > 0 ? Math.max(...levels) : 0; + const levelAvg = levels.length > 0 ? Number((levels.reduce((sum, level) => sum + level, 0) / levels.length).toFixed(1)) : 0; + return { + step: state.step, + latestBattleId: state.latestBattleId, + battleCount: Object.keys(state.battleHistory ?? {}).length, + gold: state.gold ?? 0, + supplies: { + bean: state.inventory?.[labels.bean] ?? 0, + salve: state.inventory?.[labels.salve] ?? 0, + wine: state.inventory?.[labels.wine] ?? 0 + }, + rosterCount: allies.length, + levelMin, + levelMax, + levelAvg, + lowHp: hpRatios.filter((ratio) => ratio < 0.55).length, + topLevels: allies + .map((unit) => ({ id: unit.id, level: unit.level ?? 1 })) + .sort((a, b) => b.level - a.level || a.id.localeCompare(b.id)) + .slice(0, 8) + }; + }, + { labels: supplyLabels, slotKey: campaignSlotStorageKey, storageKey: campaignStorageKey } + ); +} + async function playBattleWithoutDebugVictory(page, battle) { return page.evaluate( async ({ no, id, maxRounds, targets, protected: protectedUnitIds }) => { diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 52f2228..169b666 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -3142,7 +3142,7 @@ export const fortySeventhBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 50', '상처약 40', '탁주 17', '남중 산길 지도 1', '마을 회유 문서 1'], + itemRewards: ['콩 42', '상처약 30', '탁주 13', '남중 산길 지도 1', '마을 회유 문서 1'], victoryPages: fortySeventhBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3202,7 +3202,7 @@ export const fortyEighthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 54', '상처약 42', '탁주 18', '맹획 회유 서신 1', '남중 포로 명부 1'], + itemRewards: ['콩 44', '상처약 32', '탁주 14', '맹획 회유 서신 1', '남중 포로 명부 1'], victoryPages: fortyEighthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3262,7 +3262,7 @@ export const fortyNinthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 58', '상처약 44', '탁주 19', '칠종칠금 회유문 1', '포로 안심 장부 1'], + itemRewards: ['콩 46', '상처약 34', '탁주 15', '칠종칠금 회유문 1', '포로 안심 장부 1'], victoryPages: fortyNinthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3322,7 +3322,7 @@ export const fiftiethBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 62', '상처약 46', '탁주 20', '남중 호족 설득문 1', '세 번째 회유 장부 1'], + itemRewards: ['콩 42', '상처약 30', '탁주 14', '남중 호족 설득문 1', '세 번째 회유 장부 1'], victoryPages: fiftiethBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3389,7 +3389,7 @@ export const fiftyFirstBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 46', '상처약 34', '탁주 16', '호족 보증 장부 1', '회유 퇴로 표식 1'], + itemRewards: ['콩 34', '상처약 24', '탁주 12', '호족 보증 장부 1', '회유 퇴로 표식 1'], victoryPages: fiftyFirstBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3456,7 +3456,7 @@ export const fiftySecondBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 50', '상처약 38', '탁주 18', '강경파 분리 장부 1', '남중 회유 깃발 1'], + itemRewards: ['콩 36', '상처약 26', '탁주 13', '강경파 분리 장부 1', '남중 회유 깃발 1'], victoryPages: fiftySecondBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3523,7 +3523,7 @@ export const fiftyThirdBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 54', '상처약 42', '탁주 20', '남중 신뢰 장부 1', '계곡 회유 표식 1'], + itemRewards: ['콩 38', '상처약 28', '탁주 14', '남중 신뢰 장부 1', '계곡 회유 표식 1'], victoryPages: fiftyThirdBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3590,7 +3590,7 @@ export const fiftyFourthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 60', '상처약 46', '탁주 22', '남중 평정 장부 1', '맹획 항복 서약 1'], + itemRewards: ['콩 42', '상처약 30', '탁주 15', '남중 평정 장부 1', '맹획 항복 서약 1'], victoryPages: fiftyFourthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3657,7 +3657,7 @@ export const fiftyFifthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 86', '상처약 70', '탁주 34', '기산 출진 장부 1', '한중 보급 표식 1'], + itemRewards: ['콩 60', '상처약 44', '탁주 22', '기산 출진 장부 1', '한중 보급 표식 1'], victoryPages: fiftyFifthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3731,7 +3731,7 @@ export const fiftySixthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 90', '상처약 74', '탁주 36', '천수 진군 장부 1', '가정 지형 표식 1'], + itemRewards: ['콩 62', '상처약 46', '탁주 23', '천수 진군 장부 1', '가정 지형 표식 1'], victoryPages: fiftySixthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3815,7 +3815,7 @@ export const fiftySeventhBattleScenario: BattleScenarioDefinition = { { kind: 'unit-defeated', unitId: 'zhuge-liang' }, { kind: 'unit-defeated', unitId: 'wang-ping' } ], - itemRewards: ['콩 94', '상처약 78', '탁주 38', '가정 물길 장부 1', '강유 합류'], + itemRewards: ['콩 64', '상처약 48', '탁주 24', '가정 물길 장부 1', '강유 합류'], victoryPages: fiftySeventhBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3896,7 +3896,7 @@ export const fiftyEighthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 98', '상처약 82', '탁주 40', '기산 퇴각 지도 1', '강유 첫 출전 기록 1'], + itemRewards: ['콩 66', '상처약 50', '탁주 25', '기산 퇴각 지도 1', '강유 첫 출전 기록 1'], victoryPages: fiftyEighthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -3977,7 +3977,7 @@ export const fiftyNinthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 104', '상처약 86', '탁주 42', '진창 공성 장부 1', '왕쌍 추격 기록 1'], + itemRewards: ['콩 68', '상처약 52', '탁주 26', '진창 공성 장부 1', '왕쌍 추격 기록 1'], victoryPages: fiftyNinthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -4058,7 +4058,7 @@ export const sixtiethBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 110', '상처약 90', '탁주 44', '무도·음평 장부 1', '곽회 구원로 기록 1'], + itemRewards: ['콩 70', '상처약 54', '탁주 27', '무도·음평 장부 1', '곽회 구원로 기록 1'], victoryPages: sixtiethBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -4139,7 +4139,7 @@ export const sixtyFirstBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 118', '상처약 94', '탁주 46', '한중 우로 장부 1', '조진 역공 기록 1'], + itemRewards: ['콩 72', '상처약 56', '탁주 28', '한중 우로 장부 1', '조진 역공 기록 1'], victoryPages: sixtyFirstBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -4206,7 +4206,7 @@ export const sixtySecondBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 126', '상처약 98', '탁주 48', '기산 재출정 장부 1', '노성 지연선 기록 1'], + itemRewards: ['콩 74', '상처약 58', '탁주 29', '기산 재출정 장부 1', '노성 지연선 기록 1'], victoryPages: sixtySecondBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -4273,7 +4273,7 @@ export const sixtyThirdBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 132', '상처약 102', '탁주 50', '노성 추격 장부 1', '회수 표식 기록 1'], + itemRewards: ['콩 76', '상처약 60', '탁주 30', '노성 추격 장부 1', '회수 표식 기록 1'], victoryPages: sixtyThirdBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -4340,7 +4340,7 @@ export const sixtyFourthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 138', '상처약 106', '탁주 52', '위수 진영 장부 1', '강안 봉화 기록 1'], + itemRewards: ['콩 78', '상처약 62', '탁주 30', '위수 진영 장부 1', '강안 봉화 기록 1'], victoryPages: sixtyFourthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -4407,7 +4407,7 @@ export const sixtyFifthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 144', '상처약 110', '탁주 54', '북안 전진 장부 1', '위수 재추격 기록 1'], + itemRewards: ['콩 80', '상처약 64', '탁주 31', '북안 전진 장부 1', '위수 재추격 기록 1'], victoryPages: sixtyFifthBattleVictoryPages, nextCampScene: 'CampScene' }; @@ -4474,7 +4474,7 @@ export const sixtySixthBattleScenario: BattleScenarioDefinition = { } ], defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], - itemRewards: ['콩 150', '상처약 114', '탁주 56', '오장원 귀환 장부 1', '강유 계승 지도 1'], + itemRewards: ['콩 84', '상처약 66', '탁주 32', '오장원 귀환 장부 1', '강유 계승 지도 1'], victoryPages: sixtySixthBattleVictoryPages, nextCampScene: 'CampScene' }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 50e07f2..87d0f79 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -15516,6 +15516,17 @@ const createThirtyFirstEnemyUnit = ( rebelLeader: 4 }; const statLift = Math.max(0, level - 52) * 2; + const lateNorthernTuned = [ + 'northern-sixth-', + 'northern-seventh-', + 'northern-eighth-', + 'northern-ninth-', + 'northern-tenth-', + 'northern-eleventh-', + 'northern-twelfth-' + ].some((prefix) => id.startsWith(prefix)); + const tunedHp = lateNorthernTuned ? Math.max(1, Math.round(hp * 0.72)) : hp; + const tunedAttack = lateNorthernTuned ? Math.max(1, Math.round(attack * 0.72)) : attack; return { id, @@ -15525,9 +15536,9 @@ const createThirtyFirstEnemyUnit = ( classKey, level, exp: Math.min(98, 84 + statLift), - hp, - maxHp: hp, - attack, + hp: tunedHp, + maxHp: tunedHp, + attack: tunedAttack, move: moveByClass[classKey], stats: { might: 148 + statLift + (classKey === 'rebelLeader' ? 34 : classKey === 'cavalry' ? 24 : classKey === 'yellowTurban' ? 20 : 0), @@ -16627,13 +16638,13 @@ const fortyFourthBattleUnitsBase: UnitData[] = [ export const fortyFourthBattleUnits: UnitData[] = tuneEnemyUnitGroup(fortyFourthBattleUnitsBase, ['maicheng-'], { hpScale: 0.55, - attackScale: 0.7, + attackScale: 0.64, overrides: { - 'maicheng-officer-pan-zhang': { hp: 360, attack: 116, x: 52, y: 54 }, - 'maicheng-officer-ma-zhong': { hp: 300, attack: 104, x: 50, y: 42 }, - 'maicheng-officer-lu-xun': { hp: 280, attack: 104, x: 58, y: 48 }, - 'maicheng-officer-xu-huang': { hp: 380, attack: 118, x: 60, y: 34 }, - 'maicheng-leader-lu-meng': { hp: 430, attack: 122, x: 54, y: 55 } + 'maicheng-officer-pan-zhang': { hp: 360, attack: 108, x: 52, y: 54 }, + 'maicheng-officer-ma-zhong': { hp: 300, attack: 98, x: 50, y: 42 }, + 'maicheng-officer-lu-xun': { hp: 280, attack: 98, x: 58, y: 48 }, + 'maicheng-officer-xu-huang': { hp: 380, attack: 110, x: 60, y: 34 }, + 'maicheng-leader-lu-meng': { hp: 430, attack: 114, x: 54, y: 55 } } }); @@ -17683,25 +17694,25 @@ const fiftySixthBattleAllyPositions: Record = 'jian-yong': { x: 10, y: 86 }, 'mi-zhu': { x: 14, y: 88 }, 'sun-qian': { x: 19, y: 88 }, - 'zhao-yun': { x: 29, y: 76 }, - 'zhuge-liang': { x: 17, y: 75 }, - 'ma-liang': { x: 21, y: 72 }, + 'zhao-yun': { x: 42, y: 72 }, + 'zhuge-liang': { x: 36, y: 74 }, + 'ma-liang': { x: 39, y: 68 }, 'yi-ji': { x: 9, y: 90 }, 'gong-zhi': { x: 25, y: 88 }, 'huang-zhong': { x: 34, y: 75 }, - 'wei-yan': { x: 40, y: 79 }, + 'wei-yan': { x: 44, y: 75 }, 'pang-tong': { x: 14, y: 70 }, 'fa-zheng': { x: 25, y: 70 }, 'wu-yi': { x: 41, y: 86 }, 'yan-yan': { x: 45, y: 79 }, 'li-yan': { x: 32, y: 69 }, - 'huang-quan': { x: 11, y: 74 }, + 'huang-quan': { x: 35, y: 71 }, 'ma-chao': { x: 48, y: 75 }, - 'ma-dai': { x: 52, y: 82 }, - 'wang-ping': { x: 36, y: 70 } + 'ma-dai': { x: 48, y: 72 }, + 'wang-ping': { x: 45, y: 69 } }; -export const fiftySixthBattleUnits: UnitData[] = [ +const fiftySixthBattleUnitsBase: UnitData[] = [ ...[ ...firstBattleUnits.filter((unit) => unit.faction === 'ally' && unit.id !== 'liu-bei'), ...xuzhouRecruitUnits, @@ -17766,31 +17777,36 @@ export const fiftySixthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('northern-second-leader-guo-huai', '곽회', '위군 북방 지휘관', 'rebelLeader', 182, 1320, 316, 104, 24) ]; +export const fiftySixthBattleUnits: UnitData[] = tuneEnemyUnitGroup(fiftySixthBattleUnitsBase, ['northern-second-'], { + hpScale: 0.62, + attackScale: 0.72 +}); + const fiftySeventhBattleAllyPositions: Record = { 'guan-yu': { x: 18, y: 83 }, 'zhang-fei': { x: 24, y: 84 }, 'jian-yong': { x: 11, y: 90 }, 'mi-zhu': { x: 15, y: 91 }, 'sun-qian': { x: 20, y: 91 }, - 'zhao-yun': { x: 31, y: 80 }, - 'zhuge-liang': { x: 17, y: 79 }, - 'ma-liang': { x: 22, y: 76 }, + 'zhao-yun': { x: 42, y: 80 }, + 'zhuge-liang': { x: 38, y: 79 }, + 'ma-liang': { x: 39, y: 76 }, 'yi-ji': { x: 10, y: 93 }, 'gong-zhi': { x: 26, y: 91 }, 'huang-zhong': { x: 37, y: 79 }, - 'wei-yan': { x: 43, y: 82 }, + 'wei-yan': { x: 48, y: 75 }, 'pang-tong': { x: 14, y: 75 }, 'fa-zheng': { x: 26, y: 74 }, 'wu-yi': { x: 44, y: 89 }, 'yan-yan': { x: 48, y: 82 }, 'li-yan': { x: 34, y: 73 }, - 'huang-quan': { x: 11, y: 78 }, + 'huang-quan': { x: 38, y: 81 }, 'ma-chao': { x: 51, y: 78 }, - 'ma-dai': { x: 55, y: 85 }, - 'wang-ping': { x: 39, y: 73 } + 'ma-dai': { x: 55, y: 80 }, + 'wang-ping': { x: 58, y: 72 } }; -export const fiftySeventhBattleUnits: UnitData[] = [ +const fiftySeventhBattleUnitsBase: UnitData[] = [ ...[ ...firstBattleUnits.filter((unit) => unit.faction === 'ally' && unit.id !== 'liu-bei'), ...xuzhouRecruitUnits, @@ -17847,14 +17863,19 @@ export const fiftySeventhBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('northern-third-strategist-b', '위군 가정 군리', '고지 기록 군리', 'strategist', 184, 988, 276, 84, 39), createThirtyFirstEnemyUnit('northern-third-strategist-c', '위군 가정 군리', '보급 차단 군리', 'strategist', 184, 988, 276, 96, 54), createThirtyFirstEnemyUnit('northern-third-strategist-d', '위군 가정 군리', '장합 본영 군리', 'strategist', 185, 1000, 278, 108, 26), - createThirtyFirstEnemyUnit('northern-third-officer-jiang-wei', '강유', '흔들리는 천수 장수', 'rebelLeader', 186, 1320, 322, 87, 43), + createThirtyFirstEnemyUnit('northern-third-officer-jiang-wei', '강유', '흔들리는 천수 장수', 'rebelLeader', 186, 1320, 322, 84, 45), createThirtyFirstEnemyUnit('northern-third-officer-fei-yao', '비요', '장합 지원 장수', 'rebelLeader', 185, 1300, 318, 99, 57), createThirtyFirstEnemyUnit('northern-third-officer-ma-zun', '마준', '천수 태수', 'rebelLeader', 185, 1300, 316, 105, 64), createThirtyFirstEnemyUnit('northern-third-officer-supply', '위군 보급 장교', '물길 차단 장수', 'rebelLeader', 184, 1280, 312, 77, 71), createThirtyFirstEnemyUnit('northern-third-officer-masu-pressure', '가정 압박대', '마속 진영 압박', 'rebelLeader', 184, 1280, 312, 92, 35), - createThirtyFirstEnemyUnit('northern-third-leader-zhang-he', '장합', '위군 역습 총대장', 'rebelLeader', 187, 1400, 332, 109, 24) + createThirtyFirstEnemyUnit('northern-third-leader-zhang-he', '장합', '위군 역습 총대장', 'rebelLeader', 187, 1400, 332, 102, 34) ]; +export const fiftySeventhBattleUnits: UnitData[] = tuneEnemyUnitGroup(fiftySeventhBattleUnitsBase, ['northern-third-'], { + hpScale: 0.72, + attackScale: 0.72 +}); + const fiftyEighthBattleAllyPositions: Record = { 'guan-yu': { x: 20, y: 85 }, 'zhang-fei': { x: 26, y: 86 }, @@ -17952,26 +17973,26 @@ const fiftyNinthBattleAllyPositions: Record = 'jian-yong': { x: 10, y: 95 }, 'mi-zhu': { x: 14, y: 96 }, 'sun-qian': { x: 19, y: 96 }, - 'zhao-yun': { x: 31, y: 86 }, - 'zhuge-liang': { x: 17, y: 85 }, - 'ma-liang': { x: 22, y: 82 }, + 'zhao-yun': { x: 43, y: 84 }, + 'zhuge-liang': { x: 44, y: 82 }, + 'ma-liang': { x: 43, y: 79 }, 'yi-ji': { x: 9, y: 98 }, 'gong-zhi': { x: 27, y: 96 }, 'huang-zhong': { x: 37, y: 84 }, - 'wei-yan': { x: 43, y: 86 }, + 'wei-yan': { x: 52, y: 82 }, 'pang-tong': { x: 15, y: 81 }, - 'fa-zheng': { x: 27, y: 80 }, + 'fa-zheng': { x: 46, y: 78 }, 'wu-yi': { x: 45, y: 94 }, 'yan-yan': { x: 49, y: 87 }, 'li-yan': { x: 35, y: 79 }, - 'huang-quan': { x: 12, y: 84 }, + 'huang-quan': { x: 44, y: 84 }, 'ma-chao': { x: 52, y: 83 }, - 'ma-dai': { x: 56, y: 90 }, - 'wang-ping': { x: 40, y: 79 }, - 'jiang-wei': { x: 33, y: 82 } + 'ma-dai': { x: 56, y: 86 }, + 'wang-ping': { x: 50, y: 78 }, + 'jiang-wei': { x: 48, y: 80 } }; -export const fiftyNinthBattleUnits: UnitData[] = [ +const fiftyNinthBattleUnitsBase: UnitData[] = [ ...[ ...firstBattleUnits.filter((unit) => unit.faction === 'ally' && unit.id !== 'liu-bei'), ...xuzhouRecruitUnits, @@ -18029,14 +18050,19 @@ export const fiftyNinthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('northern-fifth-strategist-b', '위군 공성 대응 군리', '봉화 연락 군리', 'strategist', 193, 1092, 298, 86, 45), createThirtyFirstEnemyUnit('northern-fifth-strategist-c', '위군 공성 대응 군리', '학소 본영 군리', 'strategist', 193, 1092, 298, 100, 36), createThirtyFirstEnemyUnit('northern-fifth-strategist-d', '위군 공성 대응 군리', '군량 차단 군리', 'strategist', 194, 1104, 300, 106, 63), - createThirtyFirstEnemyUnit('northern-fifth-officer-wang-shuang', '왕쌍', '위군 추격 장수', 'rebelLeader', 196, 1480, 356, 87, 72), + createThirtyFirstEnemyUnit('northern-fifth-officer-wang-shuang', '왕쌍', '위군 추격 장수', 'rebelLeader', 196, 1480, 356, 78, 72), createThirtyFirstEnemyUnit('northern-fifth-officer-fei-yao', '비요', '진창 지원 장수', 'rebelLeader', 194, 1440, 348, 101, 58), createThirtyFirstEnemyUnit('northern-fifth-officer-guo-huai', '곽회', '북방 지원 장수', 'rebelLeader', 194, 1440, 348, 110, 68), createThirtyFirstEnemyUnit('northern-fifth-officer-supply', '위군 군량 차단장', '공성 진영 압박 장수', 'rebelLeader', 193, 1420, 344, 78, 76), createThirtyFirstEnemyUnit('northern-fifth-officer-wall', '진창 성벽 장수', '성문 수비 장수', 'rebelLeader', 194, 1440, 348, 112, 27), - createThirtyFirstEnemyUnit('northern-fifth-leader-hao-zhao', '학소', '진창 수비장', 'rebelLeader', 197, 1540, 362, 109, 35) + createThirtyFirstEnemyUnit('northern-fifth-leader-hao-zhao', '학소', '진창 수비장', 'rebelLeader', 197, 1540, 362, 104, 36) ]; +export const fiftyNinthBattleUnits: UnitData[] = tuneEnemyUnitGroup(fiftyNinthBattleUnitsBase, ['northern-fifth-'], { + hpScale: 0.72, + attackScale: 0.72 +}); + const sixtiethBattleAllyPositions: Record = { 'guan-yu': { x: 18, y: 90 }, 'zhang-fei': { x: 24, y: 92 }, @@ -18572,7 +18598,7 @@ const sixtyFifthEnemySpecs: ReadonlyArray< ['northern-eleventh-officer-supply', '강북 수레 차단장', '수레 차단 장수', 'rebelLeader', 247, 1920, 456, 111, 76], ['northern-eleventh-officer-guo-huai', '곽회', '북안 봉화 장수', 'rebelLeader', 246, 1900, 452, 121, 74], ['northern-eleventh-officer-relay', '봉화 연락장', '북안 뒤 봉화 장수', 'rebelLeader', 246, 1900, 452, 125, 56], - ['northern-eleventh-officer-zhang-he', '장합', '재추격 대장', 'rebelLeader', 248, 1960, 462, 126, 35], + ['northern-eleventh-officer-zhang-he', '장합', '재추격 대장', 'rebelLeader', 248, 1960, 462, 112, 52], ['northern-eleventh-leader-sima-yi', '사마의', '북안 지연 총지휘', 'rebelLeader', 249, 2000, 468, 122, 47] ]; @@ -18668,7 +18694,7 @@ const sixtySixthEnemySpecs: ReadonlyArray< ['northern-twelfth-officer-wuzhang', '오장원 전진장', '전진 진영 무장', 'rebelLeader', 252, 2000, 472, 90, 84], ['northern-twelfth-officer-ledger', '장부 차단장', '장부 차단 무장', 'rebelLeader', 252, 2000, 472, 111, 74], ['northern-twelfth-officer-guo-huai', '곽회', '봉화 차단 무장', 'rebelLeader', 252, 1980, 468, 122, 70], - ['northern-twelfth-officer-zhang-he', '장합', '추격 총대장', 'rebelLeader', 253, 2040, 478, 116, 44], + ['northern-twelfth-officer-zhang-he', '장합', '추격 총대장', 'rebelLeader', 253, 2040, 478, 108, 58], ['northern-twelfth-leader-sima-yi', '사마의', '오장원 총지휘관', 'rebelLeader', 254, 2100, 486, 124, 52] ]; @@ -23198,7 +23224,7 @@ function createFiftySixthBattleTerrain(): TerrainType[][] { Array.from({ length: 112 }, (_, x): TerrainType => { if ( (x >= 0 && x <= 111 && y >= 82) || - (x >= 76 && x <= 81 && y >= 0 && y <= 93) || + (x >= 76 && x <= 81 && y >= 0 && y <= 93 && !(y >= 43 && y <= 58)) || (x >= 0 && x <= 30 && y >= 73 && y <= 90) || (x >= 86 && x <= 107 && y >= 69 && y <= 84) ) { @@ -23274,14 +23300,15 @@ function createFiftySeventhBattleTerrain(): TerrainType[][] { Array.from({ length: 114 }, (_, x): TerrainType => { if ( (x >= 0 && x <= 113 && y >= 84) || - (x >= 30 && x <= 36 && y >= 0 && y <= 94) || - (x >= 78 && x <= 84 && y >= 0 && y <= 88) || + (x >= 30 && x <= 36 && y >= 0 && y <= 94 && !((y >= 46 && y <= 54) || (y >= 62 && y <= 83))) || + (x >= 78 && x <= 84 && y >= 0 && y <= 88 && !(y >= 44 && y <= 70)) || (x >= 88 && x <= 113 && y >= 70 && y <= 88) ) { return 'river'; } if ( (x >= 8 && x <= 110 && y >= 78 && y <= 83) || + (x >= 28 && x <= 68 && y >= 46 && y <= 54) || (x >= 28 && x <= 91 && y >= 62 && y <= 70) || (x >= 64 && x <= 110 && y >= 53 && y <= 61) || (x >= 46 && x <= 53 && y >= 54 && y <= 84) || @@ -23311,7 +23338,7 @@ function createFiftySeventhBattleTerrain(): TerrainType[][] { if ( (x <= 2 && y <= 84) || (x >= 111 && y <= 80) || - (x >= 36 && x <= 42 && y >= 0 && y <= 56) || + (x >= 36 && x <= 42 && y >= 0 && y <= 56 && !(y >= 46 && y <= 54)) || (x >= 84 && x <= 89 && y >= 0 && y <= 44) || (x >= 54 && x <= 60 && y >= 76 && y <= 95) ) { @@ -23427,7 +23454,7 @@ function createFiftyNinthBattleTerrain(): TerrainType[][] { Array.from({ length: 118 }, (_, x): TerrainType => { if ( (x >= 4 && x <= 10 && y >= 0 && y <= 99) || - (x >= 60 && x <= 67 && y >= 20 && y <= 99) || + (x >= 60 && x <= 67 && y >= 20 && y <= 99 && !(y >= 54 && y <= 90)) || (x >= 0 && x <= 117 && y >= 92) ) { return 'river'; @@ -23502,7 +23529,7 @@ function createSixtiethBattleTerrain(): TerrainType[][] { Array.from({ length: 120 }, (_, x): TerrainType => { if ( (x >= 5 && x <= 11 && y >= 0 && y <= 101) || - (x >= 62 && x <= 68 && y >= 16 && y <= 101) || + (x >= 62 && x <= 68 && y >= 16 && y <= 101 && !(y >= 55 && y <= 92)) || (x >= 0 && x <= 119 && y >= 94) ) { return 'river'; @@ -23577,7 +23604,7 @@ function createSixtyFirstBattleTerrain(): TerrainType[][] { Array.from({ length: 122 }, (_, x): TerrainType => { if ( (x >= 5 && x <= 12 && y >= 0 && y <= 103) || - (x >= 63 && x <= 70 && y >= 14 && y <= 103) || + (x >= 63 && x <= 70 && y >= 14 && y <= 103 && !(y >= 56 && y <= 94)) || (x >= 0 && x <= 121 && y >= 96) ) { return 'river'; @@ -23655,7 +23682,7 @@ function createSixtySecondBattleTerrain(): TerrainType[][] { Array.from({ length: 124 }, (_, x): TerrainType => { if ( (x >= 5 && x <= 12 && y >= 0 && y <= 105) || - (x >= 64 && x <= 71 && y >= 12 && y <= 105) || + (x >= 64 && x <= 71 && y >= 12 && y <= 105 && !(y >= 56 && y <= 96)) || (x >= 0 && x <= 123 && y >= 98) ) { return 'river'; @@ -23735,7 +23762,7 @@ function createSixtyThirdBattleTerrain(): TerrainType[][] { Array.from({ length: 126 }, (_, x): TerrainType => { if ( (x >= 6 && x <= 13 && y >= 0 && y <= 107) || - (x >= 66 && x <= 73 && y >= 10 && y <= 107) || + (x >= 66 && x <= 73 && y >= 10 && y <= 107 && !(y >= 58 && y <= 98)) || (x >= 0 && x <= 125 && y >= 100) ) { return 'river'; @@ -23816,8 +23843,8 @@ function createSixtyFourthBattleTerrain(): TerrainType[][] { return Array.from({ length: 110 }, (_, y) => Array.from({ length: 128 }, (_, x): TerrainType => { if ( - (x >= 2 && x <= 126 && y >= 21 && y <= 32) || - (x >= 2 && x <= 126 && y >= 36 && y <= 45) + (x >= 2 && x <= 126 && y >= 21 && y <= 32 && !(x >= 100 && x <= 116)) || + (x >= 2 && x <= 126 && y >= 36 && y <= 45 && !(x >= 99 && x <= 127)) ) { return 'river'; } @@ -23897,8 +23924,8 @@ function createSixtyFifthBattleTerrain(): TerrainType[][] { return Array.from({ length: 112 }, (_, y) => Array.from({ length: 130 }, (_, x): TerrainType => { if ( - (x >= 2 && x <= 128 && y >= 27 && y <= 38) || - (x >= 3 && x <= 128 && y >= 42 && y <= 52) + (x >= 2 && x <= 128 && y >= 27 && y <= 38 && !(x >= 102 && x <= 118)) || + (x >= 3 && x <= 128 && y >= 42 && y <= 52 && !(x >= 101 && x <= 129)) ) { return 'river'; } diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 1b39606..08d9a9b 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -6831,7 +6831,9 @@ export class BattleScene extends Phaser.Scene { 'northern-seventh', 'northern-eighth', 'northern-ninth', - 'northern-tenth' + 'northern-tenth', + 'northern-eleventh', + 'northern-twelfth' ]; if (lateNorthernPrefixes.some((prefix) => enemy.id.startsWith(prefix))) { if ( @@ -6839,7 +6841,9 @@ export class BattleScene extends Phaser.Scene { enemy.id.includes('-cavalry') || enemy.id === 'northern-eighth-officer-zhang-he' || enemy.id === 'northern-ninth-officer-zhang-he' || - enemy.id === 'northern-tenth-officer-zhang-he' + enemy.id === 'northern-tenth-officer-zhang-he' || + enemy.id === 'northern-eleventh-officer-zhang-he' || + enemy.id === 'northern-twelfth-officer-zhang-he' ) { return 'aggressive'; } @@ -6850,17 +6854,25 @@ export class BattleScene extends Phaser.Scene { } if ( enemy.id.startsWith('northern-first-scout') || - enemy.id.startsWith('northern-second-scout') || - enemy.id.startsWith('northern-third-scout') || enemy.id.startsWith('northern-first-cavalry') || - enemy.id.startsWith('northern-second-cavalry') || - enemy.id.startsWith('northern-third-cavalry') || - enemy.id === 'northern-first-officer-zhang-he' || - enemy.id === 'northern-second-officer-jiang-wei' || - enemy.id === 'northern-third-leader-zhang-he' + enemy.id === 'northern-first-officer-zhang-he' ) { return 'aggressive'; } + if ( + enemy.id.startsWith('northern-second-scout') || + enemy.id.startsWith('northern-second-cavalry') || + enemy.id.startsWith('northern-third-scout') || + enemy.id.startsWith('northern-third-cavalry') + ) { + return enemy.id.endsWith('-a') || enemy.id.endsWith('-b') || enemy.id.endsWith('-c') ? 'aggressive' : 'guard'; + } + if (enemy.id === 'northern-second-officer-jiang-wei') { + return 'guard'; + } + if (enemy.id === 'northern-third-leader-zhang-he') { + return 'guard'; + } if ( enemy.id.startsWith('northern-first-archer') || enemy.id.startsWith('northern-second-archer') || diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 5ccd3df..7b52296 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -223,6 +223,12 @@ export type CampaignReserveTrainingFocusDefinition = { export const defaultCampaignReserveTrainingFocusId: CampaignReserveTrainingFocusId = 'balanced'; +const campaignInventoryCaps: Record = { + ['\uCF69']: 120, + ['\uC0C1\uCC98\uC57D']: 80, + ['\uD0C1\uC8FC']: 80 +}; + export const campaignReserveTrainingFocusDefinitions: CampaignReserveTrainingFocusDefinition[] = [ { id: 'balanced', @@ -770,7 +776,9 @@ function createBattleSettlement(report: FirstBattleReport, reserveTraining: Camp function applyRewardDelta(inventory: Record, rewards: string[], direction: 1 | -1) { return rewards.reduce>((nextInventory, reward) => { const { label, amount } = parseRewardLabel(reward); - nextInventory[label] = Math.max(0, (nextInventory[label] ?? 0) + amount * direction); + const nextAmount = Math.max(0, (nextInventory[label] ?? 0) + amount * direction); + const cap = direction === 1 ? campaignInventoryCaps[label] : undefined; + nextInventory[label] = cap ? Math.min(nextAmount, cap) : nextAmount; if (nextInventory[label] === 0) { delete nextInventory[label]; }