Add tactical battle objective guidance
This commit is contained in:
@@ -7,7 +7,8 @@ import {
|
||||
getBattleScenario,
|
||||
type BattleCampaignRewardDefinition,
|
||||
type BattleObjectiveDefinition,
|
||||
type BattleScenarioDefinition
|
||||
type BattleScenarioDefinition,
|
||||
type BattleTacticalGuideEventKey
|
||||
} from '../data/battles';
|
||||
import { getTerrainRule, getUnitClass, type TerrainType, type UnitClassKey } from '../data/battleRules';
|
||||
import {
|
||||
@@ -8466,10 +8467,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
if (objective.kind === 'secure-terrain') {
|
||||
const terrain = (objective.terrain ?? 'village') as TerrainType;
|
||||
const secured =
|
||||
terrain === 'village'
|
||||
? !this.hasEnemyThreateningTerrain(terrain, 2)
|
||||
: !this.hasEnemyOnTerrain(terrain);
|
||||
const allyHolding = this.isAnyAllyNearTerrain(terrain, 1);
|
||||
const secured = allyHolding && !this.hasEnemyThreateningTerrain(terrain, 2);
|
||||
const terrainLabel = terrain === 'village' ? '마을 주변' : terrain === 'camp' ? '군영/수용소' : terrain === 'fort' ? '요새' : '목표 지형';
|
||||
const achieved = outcome ? outcome === 'victory' && secured : secured;
|
||||
return {
|
||||
@@ -8477,7 +8476,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
label: objective.label,
|
||||
achieved,
|
||||
status: achieved ? 'done' : outcome === 'defeat' ? 'failed' : 'active',
|
||||
detail: secured ? `${terrainLabel} 안전` : `${terrainLabel} 교전 중`,
|
||||
detail: secured ? `${terrainLabel} 안전` : allyHolding ? `${terrainLabel} 교전 중` : `${terrainLabel} 미확보`,
|
||||
rewardGold: objective.rewardGold
|
||||
};
|
||||
}
|
||||
@@ -10046,7 +10045,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private showOpeningBattleEvent() {
|
||||
this.triggerBattleEvent('opening', '전투 목표', battleScenario.openingObjectiveLines);
|
||||
const guide = battleScenario.tacticalGuide;
|
||||
const lines = guide ? [guide.summary, `진군: ${guide.route}`, guide.focus] : battleScenario.openingObjectiveLines;
|
||||
this.triggerBattleEvent('opening', '작전 목표', lines);
|
||||
}
|
||||
|
||||
private triggerTacticalEvent(key: BattleTacticalGuideEventKey, fallbackTitle: string, fallbackLines: string[]) {
|
||||
const event = battleScenario.tacticalGuide?.events?.[key];
|
||||
this.triggerBattleEvent(key, event?.title ?? fallbackTitle, event?.lines ?? fallbackLines);
|
||||
}
|
||||
|
||||
private triggerBattleEvent(key: string, title: string, lines: string[]) {
|
||||
@@ -10064,8 +10070,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
private showBattleEventBanner(title: string, lines: string[]) {
|
||||
this.hideBattleEventBanner();
|
||||
|
||||
const width = 520;
|
||||
const height = 106;
|
||||
const bodyLines = lines.slice(0, 2).map((line) => this.truncateBattleLogText(line, 42));
|
||||
const width = 540;
|
||||
const height = Phaser.Math.Clamp(80 + bodyLines.length * 22, 108, 126);
|
||||
const left = this.layout.gridX + Math.max(16, (this.layout.gridWidth - width) / 2);
|
||||
const top = this.layout.gridY + 18;
|
||||
const depth = 45;
|
||||
@@ -10086,12 +10093,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
titleText.setDepth(depth + 1);
|
||||
this.battleEventObjects.push(titleText);
|
||||
|
||||
const body = this.add.text(left + 20, top + 45, lines.join('\n'), {
|
||||
const body = this.add.text(left + 20, top + 45, bodyLines.join('\n'), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#d4dce6',
|
||||
lineSpacing: 4,
|
||||
wordWrap: { width: width - 40, useAdvancedWrap: true }
|
||||
lineSpacing: 5
|
||||
});
|
||||
body.setDepth(depth + 1);
|
||||
this.battleEventObjects.push(body);
|
||||
@@ -10118,49 +10124,76 @@ export class BattleScene extends Phaser.Scene {
|
||||
private checkBattleEvents() {
|
||||
const leader = battleUnits.find((unit) => unit.id === leaderUnitId);
|
||||
if (leader && leader.hp > 0 && leader.hp <= Math.ceil(leader.maxHp / 2)) {
|
||||
this.triggerBattleEvent('leader-wavering', '두령 동요', [`${leader.name}의 기세가 꺾였습니다.`, '두령을 몰아붙이면 황건적의 전열이 무너집니다.']);
|
||||
this.triggerTacticalEvent('leader-wavering', '두령 동요', [`${leader.name}의 기세가 꺾였습니다.`, '두령을 몰아붙이면 황건적의 전열이 무너집니다.']);
|
||||
}
|
||||
|
||||
const endangeredAlly = this.endangeredAlly();
|
||||
if (endangeredAlly) {
|
||||
this.triggerBattleEvent('ally-danger', '아군 위기', [
|
||||
this.triggerTacticalEvent('ally-danger', '아군 위기', [
|
||||
`${endangeredAlly.name}의 병력이 크게 줄었습니다.`,
|
||||
'유비가 퇴각하면 패배합니다. 마을과 성채 지형을 이용해 전열을 다시 잡으세요.'
|
||||
]);
|
||||
}
|
||||
|
||||
if (this.isAnyAllyNearTerrain('village', 2)) {
|
||||
this.triggerBattleEvent('village-approach', '마을 어귀', [
|
||||
'마을 주변에 황건적이 몰려 있습니다.',
|
||||
'마을의 적을 몰아내면 승리 보상이 추가됩니다.'
|
||||
const objectiveTerrain = this.tacticalObjectiveTerrain();
|
||||
const objectiveTerrainLabel = this.tacticalTerrainLabel(objectiveTerrain);
|
||||
if (this.isAnyAllyNearTerrain(objectiveTerrain, 2)) {
|
||||
this.triggerTacticalEvent('village-approach', `${objectiveTerrainLabel} 접근`, [
|
||||
`${objectiveTerrainLabel} 주변에 적이 버티고 있습니다.`,
|
||||
'주변의 적을 몰아내면 승리 보상이 추가됩니다.'
|
||||
]);
|
||||
}
|
||||
|
||||
if (!this.hasEnemyThreateningVillage()) {
|
||||
this.triggerBattleEvent('village-secured', '마을 확보', ['마을 주변의 황건적을 몰아냈습니다.', '승리 시 마을 확보 보상이 추가됩니다.']);
|
||||
if (this.triggeredBattleEvents.has('village-approach') && !this.hasEnemyThreateningTerrain(objectiveTerrain, 2)) {
|
||||
this.triggerTacticalEvent('village-secured', `${objectiveTerrainLabel} 확보`, [`${objectiveTerrainLabel} 주변의 적을 몰아냈습니다.`, '승리 시 목표 확보 보상이 추가됩니다.']);
|
||||
}
|
||||
|
||||
if (this.areUnitsDefeated(['rebel-a', 'rebel-b', 'rebel-c'])) {
|
||||
this.triggerBattleEvent('vanguard-broken', '선봉 격파', [
|
||||
const vanguardUnitIds = this.tacticalVanguardUnitIds();
|
||||
if (vanguardUnitIds.length > 0 && this.areUnitsDefeated(vanguardUnitIds)) {
|
||||
this.triggerTacticalEvent('vanguard-broken', '선봉 격파', [
|
||||
'남쪽 길목의 황건 선봉이 무너졌습니다.',
|
||||
'이제 중앙 마을과 기병의 움직임을 경계하세요.'
|
||||
]);
|
||||
}
|
||||
|
||||
if (this.isEnemyClassNearAlly('cavalry', 6)) {
|
||||
this.triggerBattleEvent('cavalry-approach', '기병 접근', [
|
||||
this.triggerTacticalEvent('cavalry-approach', '기병 접근', [
|
||||
'황건 기병이 빠르게 압박해옵니다.',
|
||||
'위험 범위를 켜고 숲이나 마을 쪽으로 전선을 잡으세요.'
|
||||
]);
|
||||
}
|
||||
|
||||
if (this.turnNumber === 2 && this.activeFaction === 'ally') {
|
||||
this.triggerBattleEvent('enemy-posture', '적 전술 변화', ['기병은 계속 접근하고, 궁병은 자리를 지키며 사격합니다.', '숲과 마을 지형을 이용해 피해를 줄이세요.']);
|
||||
this.triggerTacticalEvent('enemy-posture', '적 전술 변화', ['기병은 계속 접근하고, 궁병은 자리를 지키며 사격합니다.', '숲과 마을 지형을 이용해 피해를 줄이세요.']);
|
||||
}
|
||||
}
|
||||
|
||||
private hasEnemyThreateningVillage() {
|
||||
return this.hasEnemyThreateningTerrain('village', 2);
|
||||
private tacticalVanguardUnitIds() {
|
||||
if (battleScenario.id === 'second-battle-yellow-turban-pursuit') {
|
||||
return ['pursuit-raider-a', 'pursuit-raider-b', 'pursuit-bandit-a'];
|
||||
}
|
||||
if (battleScenario.id === 'third-battle-guangzong-road') {
|
||||
return ['guangzong-scout-a', 'guangzong-bandit-a', 'guangzong-archer-a'];
|
||||
}
|
||||
return ['rebel-a', 'rebel-b', 'rebel-c'];
|
||||
}
|
||||
|
||||
private tacticalObjectiveTerrain(): TerrainType {
|
||||
const terrain = battleScenario.objectives.find((objective) => objective.kind === 'secure-terrain')?.terrain;
|
||||
if (terrain === 'camp' || terrain === 'fort' || terrain === 'village') {
|
||||
return terrain;
|
||||
}
|
||||
return 'village';
|
||||
}
|
||||
|
||||
private tacticalTerrainLabel(terrain: TerrainType) {
|
||||
if (terrain === 'fort') {
|
||||
return '요새';
|
||||
}
|
||||
if (terrain === 'camp') {
|
||||
return '진영';
|
||||
}
|
||||
return '마을';
|
||||
}
|
||||
|
||||
private hasEnemyThreateningTerrain(terrain: TerrainType, distance: number) {
|
||||
@@ -10169,12 +10202,6 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private hasEnemyOnTerrain(terrain: TerrainType) {
|
||||
return battleUnits.some((unit) => {
|
||||
return unit.faction === 'enemy' && unit.hp > 0 && battleMap.terrain[unit.y][unit.x] === terrain;
|
||||
});
|
||||
}
|
||||
|
||||
private isAnyAllyNearTerrain(terrain: TerrainType, distance: number) {
|
||||
return battleUnits.some((unit) => {
|
||||
return unit.faction === 'ally' && unit.hp > 0 && this.isUnitNearTerrain(unit, terrain, distance);
|
||||
@@ -15027,6 +15054,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (text.includes('대기') || text.includes('턴') || text.includes('차례')) {
|
||||
return { icon: 'leadership', tone: palette.gold };
|
||||
}
|
||||
if (text.includes('작전') || text.includes('목표') || text.includes('진군') || text.includes('전술')) {
|
||||
return { icon: 'leadership', tone: palette.gold };
|
||||
}
|
||||
if (text.includes('공격') || text.includes('피해') || text.includes('→')) {
|
||||
return { icon: 'sword', tone: 0xd8b15f };
|
||||
}
|
||||
@@ -15202,15 +15232,18 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
this.trackSideObject(this.add.text(left, messageTop, '목표 현황', {
|
||||
const guideHeight = this.renderTacticalGuideCard(left, messageTop, width);
|
||||
const objectiveTop = messageTop + (guideHeight > 0 ? guideHeight + 12 : 0);
|
||||
|
||||
this.trackSideObject(this.add.text(left, objectiveTop, '목표 현황', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '17px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
const objectiveRows = Math.max(1, Math.floor((logTop - (messageTop + 26) - 8) / 28));
|
||||
const objectiveRows = Math.max(1, Math.floor((logTop - (objectiveTop + 26) - 8) / 28));
|
||||
objectiveStates.slice(0, objectiveRows).forEach((objective, index) => {
|
||||
this.renderObjectiveStateRow(objective, left, messageTop + 26 + index * 28, width);
|
||||
this.renderObjectiveStateRow(objective, left, objectiveTop + 26 + index * 28, width);
|
||||
});
|
||||
this.renderRecentActionLogPanel(left, logTop, width, logHeight);
|
||||
}
|
||||
@@ -15223,6 +15256,53 @@ export class BattleScene extends Phaser.Scene {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
private renderTacticalGuideCard(x: number, y: number, width: number) {
|
||||
const guide = battleScenario.tacticalGuide;
|
||||
if (!guide) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const height = 96;
|
||||
const bg = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x101820, 0.92));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(30);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.5);
|
||||
|
||||
const title = this.trackSideObject(this.add.text(x + 12, y + 8, '작전 흐름', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '15px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
title.setDepth(31);
|
||||
|
||||
const route = this.trackSideObject(this.add.text(x + 12, y + 31, this.truncateBattleLogText(`진군: ${guide.route}`, 34), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#d8b15f',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
route.setDepth(31);
|
||||
|
||||
const focus = this.trackSideObject(this.add.text(x + 12, y + 52, this.truncateBattleLogText(guide.focus, 34), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#d4dce6',
|
||||
maxLines: 1
|
||||
}));
|
||||
focus.setDepth(31);
|
||||
|
||||
const roles = this.trackSideObject(this.add.text(x + 12, y + 74, this.truncateBattleLogText(guide.roles.join(' · '), 36), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '11px',
|
||||
color: '#9fb0bf',
|
||||
maxLines: 1
|
||||
}));
|
||||
roles.setDepth(31);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
private updateObjectiveTracker() {
|
||||
if (!this.objectiveTrackerText || !this.objectiveTrackerSubText) {
|
||||
return;
|
||||
@@ -15240,9 +15320,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.battleOutcome === 'defeat'
|
||||
? `패배 조건 발생: ${battleScenario.defeatConditionLabel}`
|
||||
: `패배 조건: ${battleScenario.defeatConditionLabel}`;
|
||||
const routeText = battleScenario.tacticalGuide?.route;
|
||||
|
||||
this.objectiveTrackerText.setText(victoryText);
|
||||
this.objectiveTrackerSubText.setText(`${defeatText}${quickText}`);
|
||||
this.objectiveTrackerSubText.setText(routeText ? `진군: ${routeText}\n${defeatText}${quickText}` : `${defeatText}${quickText}`);
|
||||
}
|
||||
|
||||
private renderObjectiveStateRow(objective: BattleObjectiveState, x: number, y: number, width: number) {
|
||||
|
||||
Reference in New Issue
Block a user