diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 64badf1..01617dd 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -439,11 +439,15 @@ export const firstBattleScenario: BattleScenarioDefinition = { '관우와 장비로 앞쪽 길목을 열고, 마을 주변을 정리한 뒤 한석에게 압박을 모으십시오.' ], tacticalGuide: { - summary: '관우가 첫 길목을 열고, 장비가 남쪽 궁병을 흔들며, 유비는 뒤에서 회복과 생존을 챙깁니다.', + summary: '관우가 중앙 길목을 막고, 장비가 남쪽 궁병 사거리를 끊으며, 유비는 뒤에서 회복과 생존을 챙깁니다.', route: '시작 구역 → 중앙 길목 → 마을 어귀 → 두령 한석', - focus: '앞쪽 황건병 둘을 먼저 끊고 남쪽 궁병을 흔든 뒤, 마을을 확보하며 한석에게 모이십시오.', + focus: '첫 접전은 관우로 받아내고 장비로 궁병 압박을 시작한 뒤, 마을을 거쳐 한석에게 모이십시오.', roles: ['관우: 중앙 길목 전열', '장비: 남쪽 궁병 압박', '유비: 후방 지휘와 회복'], events: { + 'enemy-posture': { + title: '적 전열 확인', + lines: ['앞줄 보병은 다가오지만 기병은 깊숙한 곳에서 경계하고 있습니다.', '관우를 길목에 세우고 장비로 궁병 사거리를 끊어 보십시오.'] + }, 'village-approach': { title: '마을 어귀 접근', lines: ['마을 근처까지 전열이 올라왔습니다.', '주변 황건병을 몰아내면 보급 확보 보상이 붙습니다.'] @@ -454,7 +458,7 @@ export const firstBattleScenario: BattleScenarioDefinition = { }, 'cavalry-approach': { title: '기병 접근', - lines: ['황건 기병이 빠르게 다가옵니다.', '유비를 뒤에 두고 숲과 마을 지형에서 받아내십시오.'] + lines: ['황건 기병이 전열 가까이 들어왔습니다.', '유비를 뒤에 두고 숲과 마을 지형에서 받아내십시오.'] } } }, @@ -541,9 +545,9 @@ export const secondBattleScenario: BattleScenarioDefinition = { '세 형제를 모두 출전시키고 15턴 이내에 승리하면 추격전 명성과 추가 보상이 붙습니다.' ], tacticalGuide: { - summary: '관우가 나루 앞 숲길을 막고, 장비가 남쪽 우회로를 압박하며, 유비는 후방에서 마을 확보를 지휘합니다.', + summary: '관우가 나루 앞 숲길을 막고, 장비가 남쪽 측면으로 궁병을 압박하며, 유비는 후방에서 보급과 마을 확보를 지휘합니다.', route: '시작 구역 → 나루 길목 → 궁병 엄호선 → 북쪽 마을', - focus: '나루 앞 습격병 → 산길 복병 → 마을 궁병 순서로 정리한 뒤 한석을 몰아치십시오.', + focus: '나루 앞 습격병을 정리한 뒤 장비를 남쪽 길로 보내 궁병을 묶고, 관우가 한석 친위병을 붙잡으십시오.', roles: ['관우: 나루 전열', '장비: 남쪽 돌파', '유비: 마을 확보 지휘'], events: { 'village-approach': { @@ -560,7 +564,7 @@ export const secondBattleScenario: BattleScenarioDefinition = { }, 'enemy-posture': { title: '잔당 재정렬', - lines: ['궁병은 마을 쪽에서 엄호하고 기병은 측면을 노립니다.', '중앙을 무리하게 벌리지 말고 한 줄씩 밀어내세요.'] + lines: ['궁병은 마을 쪽에서 엄호하고 기병은 남쪽 측면을 돌아옵니다.', '중앙을 무리하게 벌리지 말고 관우와 장비의 전선을 나누십시오.'] } } }, @@ -646,14 +650,14 @@ export const thirdBattleScenario: BattleScenarioDefinition = { '18턴 이내에 승리하면 광종 구원로 확보 명성이 오릅니다.' ], tacticalGuide: { - summary: '관우가 강가 요새 앞을 받아내고, 장비가 전령 퇴로를 압박하며, 유비는 뒤에서 전열 붕괴를 막습니다.', + summary: '관우가 강가 요새 앞을 받아내고, 장비가 전령 호위 기병을 끊으며, 유비는 뒤에서 전열 붕괴를 막습니다.', route: '시작 구역 → 강가 길목 → 요새 궁병 → 전령 마원', - focus: '강가 궁병을 먼저 끊고 장비로 전령 기병의 길목을 막은 뒤, 관우가 요새 수비병을 붙잡으십시오.', + focus: '망루 궁병을 먼저 끊고 장비로 전령 기병의 길목을 막은 뒤, 관우가 요새 수비병을 붙잡으십시오.', roles: ['관우: 강가 전열', '장비: 전령 추격', '유비: 후방 지휘와 회복'], events: { 'village-approach': { - title: '강가 길목 진입', - lines: ['강가 길목에 닿았습니다.', '요새 궁병을 오래 두면 전열이 묶이니 빠르게 압박하십시오.'] + title: '요새 접근', + lines: ['요새와 망루 궁병이 전령을 감싸고 있습니다.', '장비가 기병 길목을 막고 관우가 수비병을 붙잡아야 합니다.'] }, 'vanguard-broken': { title: '척후선 붕괴', @@ -665,7 +669,7 @@ export const thirdBattleScenario: BattleScenarioDefinition = { }, 'enemy-posture': { title: '요새 방어 태세', - lines: ['궁병은 요새에서 버티고 기병은 좁은 길을 흔듭니다.', '피해가 쌓이면 유비를 뒤로 물려 회복 여지를 남기세요.'] + lines: ['망루 궁병은 요새 위에서 버티고 기병은 좁은 길을 흔듭니다.', '피해가 쌓이면 유비를 뒤로 물려 회복 여지를 남기세요.'] } } }, diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 43bdbd6..7bdb535 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -5534,8 +5534,8 @@ export const secondBattleUnits: UnitData[] = [ armor: { itemId: 'rebel-vest', level: 1, exp: 8 }, accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } }, - x: 18, - y: 3 + x: 17, + y: 5 }, { id: 'pursuit-guard-a', @@ -5840,8 +5840,8 @@ export const thirdBattleUnits: UnitData[] = [ armor: { itemId: 'cloth-armor', level: 1, exp: 10 }, accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } }, - x: 19, - y: 2 + x: 18, + y: 1 }, { id: 'guangzong-leader-ma-yuan', diff --git a/src/game/data/unitAssets.ts b/src/game/data/unitAssets.ts index 1ac6270..98482b8 100644 --- a/src/game/data/unitAssets.ts +++ b/src/game/data/unitAssets.ts @@ -47,6 +47,7 @@ const unitSheets = Object.entries(unitSheetUrls) .sort((left, right) => left.key.localeCompare(right.key)); const unitSheetAssetsByKey = new Map(unitSheets.map((sheet) => [sheet.key, sheet])); +const pendingUnitTextureKeys = new Set(); export function unitTextureVariantKeys(baseKey: string) { const prefix = `${baseKey}-`; @@ -64,33 +65,65 @@ export function hasUnitSheetAsset(key: string) { export function loadUnitSheets(scene: Phaser.Scene, keys: Iterable, onReady: () => void) { const uniqueKeys = Array.from(new Set(keys)); - const missingSheets = uniqueKeys - .filter((key) => !scene.textures.exists(key) || !scene.textures.exists(`${key}-actions`)) - .map((key) => unitSheetAssetsByKey.get(key) ?? missingUnitSheet(key)); - - if (missingSheets.length <= 0) { + if (unitSheetsReady(scene, uniqueKeys)) { onReady(); return; } - scene.load.once('complete', onReady); - missingSheets.forEach(({ key, url, actionKey, actionUrl }) => { - if (!scene.textures.exists(key)) { - scene.load.spritesheet(key, url, { - frameWidth: unitSheetFrameSize, - frameHeight: unitSheetFrameSize - }); + const pendingLoads: Array<{ key: string; url: string }> = []; + uniqueKeys.forEach((key) => { + const sheet = unitSheetAssetsByKey.get(key) ?? missingUnitSheet(key); + if (!scene.textures.exists(sheet.key) && !pendingUnitTextureKeys.has(sheet.key)) { + pendingUnitTextureKeys.add(sheet.key); + pendingLoads.push({ key: sheet.key, url: sheet.url }); } - if (!scene.textures.exists(actionKey)) { - scene.load.spritesheet(actionKey, actionUrl, { - frameWidth: unitSheetFrameSize, - frameHeight: unitSheetFrameSize - }); + if (!scene.textures.exists(sheet.actionKey) && !pendingUnitTextureKeys.has(sheet.actionKey)) { + pendingUnitTextureKeys.add(sheet.actionKey); + pendingLoads.push({ key: sheet.actionKey, url: sheet.actionUrl }); } }); + + if (pendingLoads.length <= 0) { + waitForUnitSheets(scene, uniqueKeys, onReady); + return; + } + + const clearPendingLoads = () => { + pendingLoads.forEach(({ key }) => pendingUnitTextureKeys.delete(key)); + }; + + scene.load.once('complete', () => { + clearPendingLoads(); + waitForUnitSheets(scene, uniqueKeys, onReady); + }); + scene.load.once('loaderror', clearPendingLoads); + pendingLoads.forEach(({ key, url }) => { + scene.load.spritesheet(key, url, { + frameWidth: unitSheetFrameSize, + frameHeight: unitSheetFrameSize + }); + }); scene.load.start(); } +function unitSheetsReady(scene: Phaser.Scene, keys: Iterable) { + return Array.from(keys).every((key) => scene.textures.exists(key) && scene.textures.exists(`${key}-actions`)); +} + +function waitForUnitSheets(scene: Phaser.Scene, keys: string[], onReady: () => void, attempts = 0) { + if (unitSheetsReady(scene, keys)) { + onReady(); + return; + } + + if (attempts > 200) { + const missing = keys.filter((key) => !scene.textures.exists(key) || !scene.textures.exists(`${key}-actions`)); + throw new Error(`Timed out waiting for unit sheet textures: ${missing.join(', ')}`); + } + + scene.time.delayedCall(50, () => waitForUnitSheets(scene, keys, onReady, attempts + 1)); +} + export function ensureUnitAnimations(scene: Phaser.Scene, keys: Iterable) { Array.from(new Set(keys)).forEach((key) => { if (!scene.textures.exists(key)) { diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 7cb3e8f..80b92a6 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1687,8 +1687,8 @@ const enemyAiByUnitId: Record = { 'rebel-e': 'aggressive', 'rebel-f': 'guard', 'rebel-g': 'guard', - 'rebel-cavalry-a': 'aggressive', - 'rebel-cavalry-b': 'aggressive', + 'rebel-cavalry-a': 'guard', + 'rebel-cavalry-b': 'guard', 'rebel-archer-b': 'hold', 'rebel-leader': 'guard', 'pursuit-raider-a': 'aggressive',