Clarify battle objective UI
This commit is contained in:
@@ -1446,7 +1446,11 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
|||||||
id: objective.id,
|
id: objective.id,
|
||||||
achieved: objective.achieved,
|
achieved: objective.achieved,
|
||||||
status: objective.status,
|
status: objective.status,
|
||||||
detail: objective.detail
|
category: objective.category,
|
||||||
|
summary: objective.summary,
|
||||||
|
detail: objective.detail,
|
||||||
|
failureReason: objective.failureReason ?? '',
|
||||||
|
targetTile: objective.targetTile ?? null
|
||||||
}));
|
}));
|
||||||
metrics.protectedStatus = protectedUnitIds.map((unitId) => {
|
metrics.protectedStatus = protectedUnitIds.map((unitId) => {
|
||||||
const unit = finalState.units.find((candidate) => candidate.id === unitId);
|
const unit = finalState.units.find((candidate) => candidate.id === unitId);
|
||||||
|
|||||||
@@ -1226,6 +1226,10 @@ type BattleObjectiveResult = {
|
|||||||
achieved: boolean;
|
achieved: boolean;
|
||||||
status: 'active' | 'done' | 'failed';
|
status: 'active' | 'done' | 'failed';
|
||||||
detail: string;
|
detail: string;
|
||||||
|
category: 'primary' | 'required' | 'bonus';
|
||||||
|
summary: string;
|
||||||
|
failureReason?: string;
|
||||||
|
targetTile?: { x: number; y: number; radius?: number };
|
||||||
rewardGold: number;
|
rewardGold: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3036,6 +3040,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private objectiveTrackerText?: Phaser.GameObjects.Text;
|
private objectiveTrackerText?: Phaser.GameObjects.Text;
|
||||||
private objectiveTrackerSubText?: Phaser.GameObjects.Text;
|
private objectiveTrackerSubText?: Phaser.GameObjects.Text;
|
||||||
private markers: Phaser.GameObjects.Rectangle[] = [];
|
private markers: Phaser.GameObjects.Rectangle[] = [];
|
||||||
|
private objectiveMarkerObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private rosterTab: RosterTab = 'ally';
|
private rosterTab: RosterTab = 'ally';
|
||||||
private sidePanelObjects: Phaser.GameObjects.GameObject[] = [];
|
private sidePanelObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private commandButtons: CommandButton[] = [];
|
private commandButtons: CommandButton[] = [];
|
||||||
@@ -3202,6 +3207,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private drawBattleScene() {
|
private drawBattleScene() {
|
||||||
this.drawMap();
|
this.drawMap();
|
||||||
|
this.renderObjectiveMapMarkers();
|
||||||
this.drawUnits();
|
this.drawUnits();
|
||||||
this.drawSidePanel();
|
this.drawSidePanel();
|
||||||
this.drawDebugSpritePreview();
|
this.drawDebugSpritePreview();
|
||||||
@@ -3987,6 +3993,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.positionObjectiveMapMarkers();
|
||||||
battleUnits.forEach((unit) => this.positionUnitView(unit));
|
battleUnits.forEach((unit) => this.positionUnitView(unit));
|
||||||
this.positionDeploymentOverlayObjects();
|
this.positionDeploymentOverlayObjects();
|
||||||
this.updateMiniMap();
|
this.updateMiniMap();
|
||||||
@@ -4042,6 +4049,87 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
object.setVisible(this.isTileVisible(x, y));
|
object.setVisible(this.isTileVisible(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderObjectiveMapMarkers() {
|
||||||
|
this.clearObjectiveMapMarkers();
|
||||||
|
battleScenario.objectives
|
||||||
|
.filter((objective) => objective.kind === 'secure-terrain' && objective.targetTile)
|
||||||
|
.forEach((objective) => {
|
||||||
|
const target = objective.targetTile;
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marker = this.trackObjectiveMapMarker(
|
||||||
|
this.add.rectangle(this.tileTopLeftX(target.x), this.tileTopLeftY(target.y), this.layout.tileSize, this.layout.tileSize, palette.gold, 0.07)
|
||||||
|
);
|
||||||
|
marker.setName(`objective-marker-${objective.id}`);
|
||||||
|
marker.setData('tileX', target.x);
|
||||||
|
marker.setData('tileY', target.y);
|
||||||
|
marker.setOrigin(0);
|
||||||
|
marker.setStrokeStyle(2, palette.gold, 0.68);
|
||||||
|
marker.setDepth(4.86);
|
||||||
|
if (this.mapMask) {
|
||||||
|
marker.setMask(this.mapMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = this.trackObjectiveMapMarker(
|
||||||
|
this.add.text(this.tileCenterX(target.x), this.tileTopLeftY(target.y) + 4, '목표', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#fff1c8',
|
||||||
|
fontStyle: '700',
|
||||||
|
stroke: '#05070a',
|
||||||
|
strokeThickness: 3
|
||||||
|
})
|
||||||
|
);
|
||||||
|
label.setName(`objective-label-${objective.id}`);
|
||||||
|
label.setData('tileX', target.x);
|
||||||
|
label.setData('tileY', target.y);
|
||||||
|
label.setData('tileOffsetX', this.layout.tileSize / 2);
|
||||||
|
label.setData('tileOffsetY', 4);
|
||||||
|
label.setOrigin(0.5, 0);
|
||||||
|
label.setDepth(4.92);
|
||||||
|
if (this.mapMask) {
|
||||||
|
label.setMask(this.mapMask);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.positionObjectiveMapMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearObjectiveMapMarkers() {
|
||||||
|
this.objectiveMarkerObjects.forEach((object) => object.destroy());
|
||||||
|
this.objectiveMarkerObjects = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private trackObjectiveMapMarker<T extends Phaser.GameObjects.GameObject>(object: T) {
|
||||||
|
this.objectiveMarkerObjects.push(object);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private positionObjectiveMapMarkers() {
|
||||||
|
this.objectiveMarkerObjects.forEach((object) => {
|
||||||
|
const tileX = object.getData('tileX') as number | undefined;
|
||||||
|
const tileY = object.getData('tileY') as number | undefined;
|
||||||
|
if (tileX === undefined || tileY === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetX = (object.getData('tileOffsetX') as number | undefined) ?? 0;
|
||||||
|
const offsetY = (object.getData('tileOffsetY') as number | undefined) ?? 0;
|
||||||
|
const overlay = object as Phaser.GameObjects.GameObject & {
|
||||||
|
setPosition: (x: number, y: number) => Phaser.GameObjects.GameObject;
|
||||||
|
setVisible: (value: boolean) => Phaser.GameObjects.GameObject;
|
||||||
|
input?: { enabled: boolean };
|
||||||
|
};
|
||||||
|
const visible = this.isTileVisible(tileX, tileY);
|
||||||
|
overlay.setPosition(this.tileTopLeftX(tileX) + offsetX, this.tileTopLeftY(tileY) + offsetY);
|
||||||
|
overlay.setVisible(visible);
|
||||||
|
if (overlay.input) {
|
||||||
|
overlay.input.enabled = visible;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private positionUnitView(unit: UnitData) {
|
private positionUnitView(unit: UnitData) {
|
||||||
const view = this.unitViews.get(unit.id);
|
const view = this.unitViews.get(unit.id);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
@@ -5175,7 +5263,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const top = this.sideContentTop();
|
const top = this.sideContentTop();
|
||||||
const selected = this.deploymentSelectedUnit();
|
const selected = this.deploymentSelectedUnit();
|
||||||
|
|
||||||
const header = this.trackSideObject(this.add.rectangle(left, top, width, 92, 0x101820, 0.96));
|
const header = this.trackSideObject(this.add.rectangle(left, top, width, 124, 0x101820, 0.96));
|
||||||
header.setOrigin(0);
|
header.setOrigin(0);
|
||||||
header.setStrokeStyle(1, palette.gold, 0.78);
|
header.setStrokeStyle(1, palette.gold, 0.78);
|
||||||
this.trackSideObject(this.add.text(left + 16, top + 13, '전열 배치', {
|
this.trackSideObject(this.add.text(left + 16, top + 13, '전열 배치', {
|
||||||
@@ -5184,14 +5272,16 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
color: '#f4dfad',
|
color: '#f4dfad',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
this.trackSideObject(this.add.text(left + 16, top + 48, this.deploymentSubtitle(), {
|
this.trackSideObject(this.add.text(left + 16, top + 47, this.deploymentSubtitle(), {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '13px',
|
fontSize: '12px',
|
||||||
color: '#c8d2dd',
|
color: '#c8d2dd',
|
||||||
wordWrap: { width: width - 32, useAdvancedWrap: true }
|
wordWrap: { width: width - 32, useAdvancedWrap: true },
|
||||||
|
maxLines: 2
|
||||||
}));
|
}));
|
||||||
|
this.renderDeploymentObjectiveBrief(left + 16, top + 82, width - 32);
|
||||||
|
|
||||||
const noticeTop = top + 104;
|
const noticeTop = top + 136;
|
||||||
const notice = this.trackSideObject(this.add.rectangle(left, noticeTop, width, 54, 0x0b1118, 0.94));
|
const notice = this.trackSideObject(this.add.rectangle(left, noticeTop, width, 54, 0x0b1118, 0.94));
|
||||||
notice.setOrigin(0);
|
notice.setOrigin(0);
|
||||||
notice.setStrokeStyle(1, selected ? palette.blue : 0x647485, selected ? 0.74 : 0.56);
|
notice.setStrokeStyle(1, selected ? palette.blue : 0x647485, selected ? 0.74 : 0.56);
|
||||||
@@ -5242,6 +5332,34 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.renderDeploymentButton(left, panelY + panelHeight - 116, width, '추천 배치 복원', 0x647485, () => this.restoreRecommendedDeployment());
|
this.renderDeploymentButton(left, panelY + panelHeight - 116, width, '추천 배치 복원', 0x647485, () => this.restoreRecommendedDeployment());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderDeploymentObjectiveBrief(x: number, y: number, width: number) {
|
||||||
|
const primary = battleScenario.objectives
|
||||||
|
.filter((objective) => this.objectiveCategory(objective) !== 'bonus')
|
||||||
|
.map((objective) => `${this.objectiveCategoryLabel(this.objectiveCategory(objective))}: ${objective.label}`)
|
||||||
|
.join(' / ');
|
||||||
|
const bonus = battleScenario.objectives
|
||||||
|
.filter((objective) => this.objectiveCategory(objective) === 'bonus')
|
||||||
|
.map((objective) => objective.label)
|
||||||
|
.join(' / ');
|
||||||
|
|
||||||
|
const primaryText = this.trackSideObject(this.add.text(x, y, this.truncateBattleLogText(primary, 34), {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#f4dfad',
|
||||||
|
fontStyle: '700',
|
||||||
|
fixedWidth: width
|
||||||
|
}));
|
||||||
|
primaryText.setDepth(32);
|
||||||
|
|
||||||
|
const bonusText = this.trackSideObject(this.add.text(x, y + 18, bonus ? this.truncateBattleLogText(`보조: ${bonus}`, 36) : '보조: 전장 상황에 맞춰 추가 공훈을 노립니다.', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#9fb0bf',
|
||||||
|
fixedWidth: width
|
||||||
|
}));
|
||||||
|
bonusText.setDepth(32);
|
||||||
|
}
|
||||||
|
|
||||||
private deploymentRoleSummaries() {
|
private deploymentRoleSummaries() {
|
||||||
const recommendationByUnitId = new Map((battleScenario.sortie?.recommendedUnits ?? []).map((entry) => [entry.unitId, entry]));
|
const recommendationByUnitId = new Map((battleScenario.sortie?.recommendedUnits ?? []).map((entry) => [entry.unitId, entry]));
|
||||||
const orderByUnitId = new Map(this.deploymentSlots().filter((slot) => slot.unitId).map((slot, index) => [slot.unitId, index]));
|
const orderByUnitId = new Map(this.deploymentSlots().filter((slot) => slot.unitId).map((slot, index) => [slot.unitId, index]));
|
||||||
@@ -8551,6 +8669,26 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.resultSettlementText = undefined;
|
this.resultSettlementText = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private objectiveCategory(objective: BattleObjectiveDefinition): BattleObjectiveResult['category'] {
|
||||||
|
if (objective.kind === 'defeat-leader' || objective.kind === 'pacify-leader') {
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
if (objective.kind === 'keep-unit-alive') {
|
||||||
|
return 'required';
|
||||||
|
}
|
||||||
|
return 'bonus';
|
||||||
|
}
|
||||||
|
|
||||||
|
private objectiveCategoryLabel(category: BattleObjectiveResult['category']) {
|
||||||
|
if (category === 'primary') {
|
||||||
|
return '주 목표';
|
||||||
|
}
|
||||||
|
if (category === 'required') {
|
||||||
|
return '필수';
|
||||||
|
}
|
||||||
|
return '보조';
|
||||||
|
}
|
||||||
|
|
||||||
private resultObjectives(outcome: BattleOutcome): BattleObjectiveResult[] {
|
private resultObjectives(outcome: BattleOutcome): BattleObjectiveResult[] {
|
||||||
return this.objectiveStates(outcome).map((objective) => ({
|
return this.objectiveStates(outcome).map((objective) => ({
|
||||||
id: objective.id,
|
id: objective.id,
|
||||||
@@ -8558,6 +8696,10 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
achieved: objective.achieved,
|
achieved: objective.achieved,
|
||||||
status: objective.status,
|
status: objective.status,
|
||||||
detail: objective.detail,
|
detail: objective.detail,
|
||||||
|
category: objective.category,
|
||||||
|
summary: objective.summary,
|
||||||
|
failureReason: objective.failureReason,
|
||||||
|
targetTile: objective.targetTile ? { ...objective.targetTile } : undefined,
|
||||||
rewardGold: objective.rewardGold
|
rewardGold: objective.rewardGold
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -8567,6 +8709,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private objectiveState(objective: BattleObjectiveDefinition, outcome?: BattleOutcome): BattleObjectiveState {
|
private objectiveState(objective: BattleObjectiveDefinition, outcome?: BattleOutcome): BattleObjectiveState {
|
||||||
|
const category = this.objectiveCategory(objective);
|
||||||
|
|
||||||
if (objective.kind === 'defeat-leader' || objective.kind === 'pacify-leader') {
|
if (objective.kind === 'defeat-leader' || objective.kind === 'pacify-leader') {
|
||||||
const targetId = objective.unitId ?? leaderUnitId;
|
const targetId = objective.unitId ?? leaderUnitId;
|
||||||
const target = battleUnits.find((unit) => unit.id === targetId);
|
const target = battleUnits.find((unit) => unit.id === targetId);
|
||||||
@@ -8579,6 +8723,10 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
achieved,
|
achieved,
|
||||||
status: achieved ? 'done' : outcome === 'defeat' ? 'failed' : 'active',
|
status: achieved ? 'done' : outcome === 'defeat' ? 'failed' : 'active',
|
||||||
detail: defeated ? `${this.unitName(targetId)} ${doneText}` : `${this.unitName(targetId)} 병력 ${target?.hp ?? 0}/${target?.maxHp ?? 0}`,
|
detail: defeated ? `${this.unitName(targetId)} ${doneText}` : `${this.unitName(targetId)} 병력 ${target?.hp ?? 0}/${target?.maxHp ?? 0}`,
|
||||||
|
category,
|
||||||
|
summary: defeated ? '완료' : '진행',
|
||||||
|
failureReason: outcome === 'defeat' && !defeated ? `${this.unitName(targetId)} 미격파` : undefined,
|
||||||
|
targetTile: target ? { x: target.x, y: target.y, radius: 0 } : undefined,
|
||||||
rewardGold: objective.rewardGold
|
rewardGold: objective.rewardGold
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8592,6 +8740,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
achieved: alive,
|
achieved: alive,
|
||||||
status: alive ? (outcome === 'victory' ? 'done' : 'active') : 'failed',
|
status: alive ? (outcome === 'victory' ? 'done' : 'active') : 'failed',
|
||||||
detail: alive ? `${this.unitName(targetId)} 생존` : `${this.unitName(targetId)} 퇴각`,
|
detail: alive ? `${this.unitName(targetId)} 생존` : `${this.unitName(targetId)} 퇴각`,
|
||||||
|
category,
|
||||||
|
summary: alive ? '유지' : '실패',
|
||||||
|
failureReason: alive ? undefined : `${this.unitName(targetId)} 퇴각`,
|
||||||
rewardGold: objective.rewardGold
|
rewardGold: objective.rewardGold
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8600,15 +8751,28 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const terrain = (objective.terrain ?? 'village') as TerrainType;
|
const terrain = (objective.terrain ?? 'village') as TerrainType;
|
||||||
const allyHolding = this.isObjectiveTileHeld(objective, terrain, 1);
|
const allyHolding = this.isObjectiveTileHeld(objective, terrain, 1);
|
||||||
const enemiesAlive = battleUnits.some((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
const enemiesAlive = battleUnits.some((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
||||||
const secured = (allyHolding || !enemiesAlive) && !this.hasEnemyThreateningObjective(objective, terrain, 2);
|
const threateningEnemies = this.enemiesThreateningObjective(objective, terrain, 2);
|
||||||
|
const secured = (allyHolding || !enemiesAlive) && threateningEnemies.length === 0;
|
||||||
const terrainLabel = terrain === 'village' ? '마을 주변' : terrain === 'camp' ? '군영/수용소' : terrain === 'fort' ? '요새' : '목표 지형';
|
const terrainLabel = terrain === 'village' ? '마을 주변' : terrain === 'camp' ? '군영/수용소' : terrain === 'fort' ? '요새' : '목표 지형';
|
||||||
const achieved = outcome ? outcome === 'victory' && secured : secured;
|
const achieved = outcome ? outcome === 'victory' && secured : secured;
|
||||||
|
const summary = secured ? '확보' : allyHolding ? '교전 중' : '미확보';
|
||||||
|
const failureReason = achieved
|
||||||
|
? undefined
|
||||||
|
: threateningEnemies.length > 0
|
||||||
|
? `${terrainLabel} 적 ${threateningEnemies.length}명 잔존`
|
||||||
|
: allyHolding
|
||||||
|
? `${terrainLabel} 교전 지속`
|
||||||
|
: `${terrainLabel}에 아군 미도달`;
|
||||||
return {
|
return {
|
||||||
id: objective.id,
|
id: objective.id,
|
||||||
label: objective.label,
|
label: objective.label,
|
||||||
achieved,
|
achieved,
|
||||||
status: achieved ? 'done' : outcome === 'defeat' ? 'failed' : 'active',
|
status: achieved ? 'done' : outcome ? 'failed' : 'active',
|
||||||
detail: secured ? `${terrainLabel} 안전` : allyHolding ? `${terrainLabel} 교전 중` : `${terrainLabel} 미확보`,
|
detail: secured ? `${terrainLabel} 확보` : allyHolding ? `${terrainLabel} 교전 중` : `${terrainLabel} 미확보`,
|
||||||
|
category,
|
||||||
|
summary,
|
||||||
|
failureReason: outcome ? failureReason : undefined,
|
||||||
|
targetTile: objective.targetTile ? { ...objective.targetTile } : undefined,
|
||||||
rewardGold: objective.rewardGold
|
rewardGold: objective.rewardGold
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8622,6 +8786,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
achieved,
|
achieved,
|
||||||
status: achieved ? (outcome === 'victory' ? 'done' : 'active') : 'failed',
|
status: achieved ? (outcome === 'victory' ? 'done' : 'active') : 'failed',
|
||||||
detail: inTime ? (outcome === 'victory' ? `${this.turnNumber}/${maxTurn}턴 달성` : `${this.turnNumber}/${maxTurn}턴 진행 중`) : `${this.turnNumber}/${maxTurn}턴 초과`,
|
detail: inTime ? (outcome === 'victory' ? `${this.turnNumber}/${maxTurn}턴 달성` : `${this.turnNumber}/${maxTurn}턴 진행 중`) : `${this.turnNumber}/${maxTurn}턴 초과`,
|
||||||
|
category,
|
||||||
|
summary: inTime ? (outcome === 'victory' ? '달성' : '가능') : '초과',
|
||||||
|
failureReason: outcome && !inTime ? `${this.turnNumber - maxTurn}턴 초과` : undefined,
|
||||||
rewardGold: objective.rewardGold
|
rewardGold: objective.rewardGold
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8770,9 +8937,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
objectives.forEach((objective, index) => {
|
objectives.forEach((objective, index) => {
|
||||||
const rowY = y + 42 + index * 21;
|
const rowY = y + 42 + index * 21;
|
||||||
const mark = objective.achieved ? '완료' : objective.status === 'failed' ? '미획득' : '보너스';
|
const category = this.objectiveCategoryLabel(objective.category);
|
||||||
|
const mark = objective.achieved ? objective.summary || '완료' : objective.failureReason ? '미달' : '미획득';
|
||||||
const color = objective.achieved ? '#a8ffd0' : '#aeb7c2';
|
const color = objective.achieved ? '#a8ffd0' : '#aeb7c2';
|
||||||
const line = this.trackResultObject(this.add.text(x + 16, rowY, `${mark} ${objective.label} · ${objective.detail}`, {
|
const detail = objective.achieved ? objective.detail : objective.failureReason ?? objective.detail;
|
||||||
|
const line = this.trackResultObject(this.add.text(x + 16, rowY, `${category} · ${mark} ${objective.label} · ${detail}`, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color,
|
color,
|
||||||
@@ -10334,19 +10503,22 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return '마을';
|
return '마을';
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasEnemyThreateningTerrain(terrain: TerrainType, distance: number) {
|
private hasEnemyThreateningObjective(objective: BattleObjectiveDefinition | undefined, terrain: TerrainType, distance: number) {
|
||||||
return battleUnits.some((unit) => {
|
return this.enemiesThreateningObjective(objective, terrain, distance).length > 0;
|
||||||
return unit.faction === 'enemy' && unit.hp > 0 && this.isUnitNearTerrain(unit, terrain, distance);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasEnemyThreateningObjective(objective: BattleObjectiveDefinition | undefined, terrain: TerrainType, distance: number) {
|
private enemiesThreateningObjective(objective: BattleObjectiveDefinition | undefined, terrain: TerrainType, distance: number) {
|
||||||
const target = objective?.targetTile;
|
const target = objective?.targetTile;
|
||||||
|
const threatDistance = objective?.threatRadius ?? distance;
|
||||||
if (target) {
|
if (target) {
|
||||||
return this.isEnemyNearTile(target.x, target.y, objective.threatRadius ?? distance);
|
return battleUnits.filter((unit) => {
|
||||||
|
return unit.faction === 'enemy' && unit.hp > 0 && this.tileDistanceTo(unit.x, unit.y, target.x, target.y) <= threatDistance;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.hasEnemyThreateningTerrain(terrain, objective?.threatRadius ?? distance);
|
return battleUnits.filter((unit) => {
|
||||||
|
return unit.faction === 'enemy' && unit.hp > 0 && this.isUnitNearTerrain(unit, terrain, threatDistance);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAnyAllyNearTerrain(terrain: TerrainType, distance: number) {
|
private isAnyAllyNearTerrain(terrain: TerrainType, distance: number) {
|
||||||
@@ -10370,12 +10542,6 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private isEnemyNearTile(x: number, y: number, distance: number) {
|
|
||||||
return battleUnits.some((unit) => {
|
|
||||||
return unit.faction === 'enemy' && unit.hp > 0 && this.tileDistanceTo(unit.x, unit.y, x, y) <= distance;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private isUnitNearTerrain(unit: UnitData, terrain: TerrainType, distance: number) {
|
private isUnitNearTerrain(unit: UnitData, terrain: TerrainType, distance: number) {
|
||||||
return battleMap.terrain.some((row, y) => {
|
return battleMap.terrain.some((row, y) => {
|
||||||
return row.some((tile, x) => tile === terrain && this.tileDistanceTo(unit.x, unit.y, x, y) <= distance);
|
return row.some((tile, x) => tile === terrain && this.tileDistanceTo(unit.x, unit.y, x, y) <= distance);
|
||||||
@@ -15480,16 +15646,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const guideHeight = this.renderTacticalGuideCard(left, messageTop, width);
|
const guideHeight = this.renderTacticalGuideCard(left, messageTop, width);
|
||||||
const objectiveTop = messageTop + (guideHeight > 0 ? guideHeight + 12 : 0);
|
const objectiveTop = messageTop + (guideHeight > 0 ? guideHeight + 12 : 0);
|
||||||
|
|
||||||
this.trackSideObject(this.add.text(left, objectiveTop, '목표 현황', {
|
this.renderObjectiveStateGroups(objectiveStates, left, objectiveTop, width, Math.max(72, logTop - objectiveTop - 8));
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
||||||
fontSize: '17px',
|
|
||||||
color: '#f2e3bf',
|
|
||||||
fontStyle: '700'
|
|
||||||
}));
|
|
||||||
const objectiveRows = Math.max(1, Math.floor((logTop - (objectiveTop + 26) - 8) / 28));
|
|
||||||
objectiveStates.slice(0, objectiveRows).forEach((objective, index) => {
|
|
||||||
this.renderObjectiveStateRow(objective, left, objectiveTop + 26 + index * 28, width);
|
|
||||||
});
|
|
||||||
this.renderRecentActionLogPanel(left, logTop, width, logHeight);
|
this.renderRecentActionLogPanel(left, logTop, width, logHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15555,8 +15712,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
const leader = battleUnits.find((unit) => unit.id === leaderUnitId);
|
const leader = battleUnits.find((unit) => unit.id === leaderUnitId);
|
||||||
const leaderProgress = leader && leader.hp > 0 ? `${leader.hp}/${leader.maxHp}` : '완료';
|
const leaderProgress = leader && leader.hp > 0 ? `${leader.hp}/${leader.maxHp}` : '완료';
|
||||||
const quickObjective = this.objectiveStates(this.battleOutcome).find((objective) => objective.id === 'quick');
|
const bonusObjectives = this.objectiveStates(this.battleOutcome).filter((objective) => objective.category === 'bonus');
|
||||||
const quickText = quickObjective ? ` · 보너스: ${quickObjective.label} ${this.objectiveStatusText(quickObjective)}` : '';
|
const bonusSummary = bonusObjectives.map((objective) => `${objective.label} ${this.objectiveStatusText(objective)}`).join(' / ');
|
||||||
|
const bonusText = bonusObjectives.length > 0
|
||||||
|
? ` · 보조: ${this.truncateBattleLogText(bonusSummary, 38)}`
|
||||||
|
: '';
|
||||||
const victoryText =
|
const victoryText =
|
||||||
this.battleOutcome === 'victory'
|
this.battleOutcome === 'victory'
|
||||||
? `승리 목표 완료: ${battleScenario.victoryConditionLabel}`
|
? `승리 목표 완료: ${battleScenario.victoryConditionLabel}`
|
||||||
@@ -15568,48 +15728,103 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const routeText = battleScenario.tacticalGuide?.route;
|
const routeText = battleScenario.tacticalGuide?.route;
|
||||||
|
|
||||||
this.objectiveTrackerText.setText(victoryText);
|
this.objectiveTrackerText.setText(victoryText);
|
||||||
this.objectiveTrackerSubText.setText(routeText ? `진군: ${routeText}\n${defeatText}${quickText}` : `${defeatText}${quickText}`);
|
this.objectiveTrackerSubText.setText(routeText ? `진군: ${routeText}\n${defeatText}${bonusText}` : `${defeatText}${bonusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderObjectiveStateGroups(objectives: BattleObjectiveState[], x: number, y: number, width: number, maxHeight: number) {
|
||||||
|
const groups = [
|
||||||
|
{
|
||||||
|
label: '핵심 목표',
|
||||||
|
objectives: objectives.filter((objective) => objective.category !== 'bonus')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '보조 공훈',
|
||||||
|
objectives: objectives.filter((objective) => objective.category === 'bonus')
|
||||||
|
}
|
||||||
|
].filter((group) => group.objectives.length > 0);
|
||||||
|
let cursorY = y;
|
||||||
|
let remaining = maxHeight;
|
||||||
|
const headerHeight = 22;
|
||||||
|
const rowHeight = 36;
|
||||||
|
|
||||||
|
groups.forEach((group, groupIndex) => {
|
||||||
|
if (remaining < headerHeight + rowHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = this.trackSideObject(this.add.text(x, cursorY, group.label, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: groupIndex === 0 ? '#f2e3bf' : '#d8b15f',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
title.setDepth(31);
|
||||||
|
cursorY += headerHeight;
|
||||||
|
remaining -= headerHeight;
|
||||||
|
|
||||||
|
group.objectives.forEach((objective) => {
|
||||||
|
if (remaining < rowHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.renderObjectiveStateRow(objective, x, cursorY, width);
|
||||||
|
cursorY += rowHeight;
|
||||||
|
remaining -= rowHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
cursorY += 5;
|
||||||
|
remaining -= 5;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderObjectiveStateRow(objective: BattleObjectiveState, x: number, y: number, width: number) {
|
private renderObjectiveStateRow(objective: BattleObjectiveState, x: number, y: number, width: number) {
|
||||||
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 25, 0x101820, 0.86));
|
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 32, 0x101820, 0.88));
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setStrokeStyle(1, this.objectiveStatusStroke(objective), 0.5);
|
bg.setStrokeStyle(1, this.objectiveStatusStroke(objective), 0.5);
|
||||||
|
|
||||||
this.trackSideObject(this.add.text(x + 10, y + 5, this.objectiveStatusText(objective), {
|
const categoryColor = objective.category === 'primary' ? palette.gold : objective.category === 'required' ? palette.red : palette.blue;
|
||||||
|
const categoryBg = this.trackSideObject(this.add.rectangle(x + 28, y + 16, 48, 20, 0x0a0f14, 0.92));
|
||||||
|
categoryBg.setStrokeStyle(1, categoryColor, 0.58);
|
||||||
|
|
||||||
|
const categoryText = this.trackSideObject(this.add.text(x + 28, y + 16, this.objectiveCategoryLabel(objective.category), {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: objective.category === 'bonus' ? '#b9d7ff' : '#f4dfad',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
categoryText.setOrigin(0.5);
|
||||||
|
|
||||||
|
this.trackSideObject(this.add.text(x + 62, y + 4, objective.label, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#d4dce6',
|
||||||
|
fontStyle: '700',
|
||||||
|
fixedWidth: width - 136
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.trackSideObject(this.add.text(x + 62, y + 18, this.truncateBattleLogText(objective.failureReason ?? objective.detail, 28), {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: objective.failureReason ? '#ffb6a6' : '#9fb0bf',
|
||||||
|
fixedWidth: width - 136
|
||||||
|
}));
|
||||||
|
|
||||||
|
const status = this.trackSideObject(this.add.text(x + width - 10, y + 16, this.objectiveStatusText(objective), {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: this.objectiveStatusColor(objective),
|
color: this.objectiveStatusColor(objective),
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
this.trackSideObject(this.add.text(x + 54, y + 4, objective.label, {
|
status.setOrigin(1, 0.5);
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
||||||
fontSize: '13px',
|
|
||||||
color: '#d4dce6',
|
|
||||||
fontStyle: '700'
|
|
||||||
}));
|
|
||||||
const detail = this.trackSideObject(this.add.text(x + width - 10, y + 4, objective.detail, {
|
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#9fb0bf'
|
|
||||||
}));
|
|
||||||
detail.setOrigin(1, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private objectiveStatusText(objective: BattleObjectiveState) {
|
private objectiveStatusText(objective: BattleObjectiveState) {
|
||||||
if (objective.status === 'done') {
|
if (objective.status === 'done') {
|
||||||
return '완료';
|
return objective.summary || '완료';
|
||||||
}
|
}
|
||||||
if (objective.status === 'failed') {
|
if (objective.status === 'failed') {
|
||||||
return '실패';
|
return objective.failureReason ? '미달' : objective.summary || '실패';
|
||||||
}
|
}
|
||||||
if (objective.id === 'liu-bei') {
|
return objective.summary || '진행';
|
||||||
return '유지';
|
|
||||||
}
|
|
||||||
if (objective.id === 'quick') {
|
|
||||||
return '가능';
|
|
||||||
}
|
|
||||||
return '진행';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private objectiveStatusColor(objective: BattleObjectiveState) {
|
private objectiveStatusColor(objective: BattleObjectiveState) {
|
||||||
@@ -15619,6 +15834,12 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
if (objective.status === 'failed') {
|
if (objective.status === 'failed') {
|
||||||
return '#ffb6a6';
|
return '#ffb6a6';
|
||||||
}
|
}
|
||||||
|
if (objective.summary === '교전 중') {
|
||||||
|
return '#ffd78a';
|
||||||
|
}
|
||||||
|
if (objective.summary === '미확보') {
|
||||||
|
return '#9fb0bf';
|
||||||
|
}
|
||||||
return '#f4dfad';
|
return '#f4dfad';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ export type BattleObjectiveSnapshot = {
|
|||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
achieved: boolean;
|
achieved: boolean;
|
||||||
|
status?: 'active' | 'done' | 'failed';
|
||||||
detail: string;
|
detail: string;
|
||||||
|
category?: 'primary' | 'required' | 'bonus';
|
||||||
|
summary?: string;
|
||||||
|
failureReason?: string;
|
||||||
|
targetTile?: { x: number; y: number; radius?: number };
|
||||||
rewardGold: number;
|
rewardGold: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user