Surface terrain effects in battle UI
This commit is contained in:
@@ -1341,6 +1341,7 @@ type CombatPreview = {
|
||||
criticalRate: number;
|
||||
terrainDefenseBonus: number;
|
||||
terrainHitPenalty: number;
|
||||
terrainAffinity: number;
|
||||
counterAvailable: boolean;
|
||||
terrainLabel: string;
|
||||
matchupLabel: string;
|
||||
@@ -2920,18 +2921,6 @@ const factionLabels: Record<ActiveFaction, string> = {
|
||||
enemy: '적군'
|
||||
};
|
||||
|
||||
const terrainDisplayLabels: Record<TerrainType, string> = {
|
||||
plain: '평지',
|
||||
road: '길',
|
||||
forest: '숲',
|
||||
hill: '언덕',
|
||||
village: '마을',
|
||||
fort: '요새',
|
||||
camp: '진영',
|
||||
river: '강',
|
||||
cliff: '절벽'
|
||||
};
|
||||
|
||||
const mapMenuLabels: Partial<Record<MapMenuAction, string>> = {
|
||||
endTurn: '턴 종료',
|
||||
threat: '위험 범위',
|
||||
@@ -4136,13 +4125,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.updateUnitPointerFeedback(unit, tile, force);
|
||||
return;
|
||||
}
|
||||
this.updateTerrainPointerFeedback(tile, force);
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearPointerFeedback();
|
||||
}
|
||||
|
||||
private updateUnitPointerFeedback(unit: UnitData, tile: { x: number; y: number }, force: boolean) {
|
||||
const key = `unit:${unit.id}:${tile.x},${tile.y}:${this.activeFaction}:${this.actedUnitIds.has(unit.id)}`;
|
||||
const terrain = battleMap.terrain[tile.y][tile.x];
|
||||
const key = `unit:${unit.id}:${tile.x},${tile.y}:${this.activeFaction}:${this.actedUnitIds.has(unit.id)}:${terrain}`;
|
||||
if (!force && this.lastPointerFeedbackKey === key) {
|
||||
return;
|
||||
}
|
||||
@@ -4150,34 +4142,58 @@ export class BattleScene extends Phaser.Scene {
|
||||
const acted = this.actedUnitIds.has(unit.id);
|
||||
const selectable = unit.faction === 'ally' && this.activeFaction === 'ally' && !acted;
|
||||
const tone = selectable ? palette.blue : unit.faction === 'enemy' ? 0xd8732c : 0x647485;
|
||||
const terrainHint = this.terrainEffectText(terrain, unit, 'compact');
|
||||
const text = selectable
|
||||
? `${unit.name}: 선택 가능 · 클릭하면 이동 범위 표시`
|
||||
? `${unit.name}: 선택 가능 · ${terrainHint}`
|
||||
: unit.faction === 'enemy'
|
||||
? `${unit.name}: 적 부대 · 클릭하면 위협 범위 확인`
|
||||
: `${unit.name}: 행동 완료`;
|
||||
? `${unit.name}: 적 부대 · ${terrainHint}`
|
||||
: `${unit.name}: 행동 완료 · ${terrainHint}`;
|
||||
|
||||
this.showPointerFeedback(tile, tone, text, key, 0x1c7ed6, 0.1, 0.8);
|
||||
}
|
||||
|
||||
private updateTerrainPointerFeedback(tile: { x: number; y: number }, force: boolean) {
|
||||
const terrain = battleMap.terrain[tile.y][tile.x];
|
||||
const key = `terrain:${tile.x},${tile.y}:${terrain}`;
|
||||
if (!force && this.lastPointerFeedbackKey === key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const terrainRule = getTerrainRule(terrain);
|
||||
const tone = this.terrainTone(terrain);
|
||||
const text = `${terrainRule.label}: ${this.terrainEffectText(terrain, undefined, 'compact')} · 클릭하면 상세`;
|
||||
this.showPointerFeedback(tile, tone, text, key, terrainRule.color, terrainRule.passable === false ? 0.16 : 0.1, 0.68);
|
||||
this.renderTerrainDetail(tile.x, tile.y);
|
||||
}
|
||||
|
||||
private updateMovePointerFeedback(unit: UnitData, tile: { x: number; y: number }, force: boolean) {
|
||||
const destination = this.unitAtTile(tile.x, tile.y, unit.id);
|
||||
const canMove = this.canMoveTo(unit, tile.x, tile.y);
|
||||
const key = `move:${unit.id}:${tile.x},${tile.y}:${canMove}:${destination?.id ?? 'empty'}`;
|
||||
const terrain = battleMap.terrain[tile.y][tile.x];
|
||||
const moveCost = this.movementCostTo(unit, tile.x, tile.y);
|
||||
const key = `move:${unit.id}:${tile.x},${tile.y}:${canMove}:${destination?.id ?? 'empty'}:${moveCost ?? 'x'}:${terrain}`;
|
||||
if (!force && this.lastPointerFeedbackKey === key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canMove) {
|
||||
const distance = this.movementTileDistance(unit, tile.x, tile.y);
|
||||
const spent = moveCost ?? 0;
|
||||
const remaining = Math.max(0, unit.move - spent);
|
||||
const terrainHint = this.terrainEffectText(terrain, unit, 'move');
|
||||
const text =
|
||||
distance === 0
|
||||
? `${unit.name}: 제자리에서 명령 선택`
|
||||
: `${unit.name}: 이동 가능 · ${distance}칸 · 클릭해 확정`;
|
||||
spent === 0
|
||||
? `${unit.name}: 제자리 명령 · ${terrainHint}`
|
||||
: `${unit.name}: 이동 가능 · 소모 ${spent}/${unit.move} · 남은 ${remaining} · ${terrainHint}`;
|
||||
this.showPointerFeedback(tile, palette.blue, text, key, 0x1c7ed6, 0.12, 0.9);
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = destination ? `${destination.name}이 있는 칸` : '이동 범위 밖';
|
||||
const terrainRule = getTerrainRule(terrain);
|
||||
const reason = destination
|
||||
? `${destination.name} 배치 칸`
|
||||
: terrainRule.passable === false
|
||||
? `${terrainRule.label} 진입 불가`
|
||||
: `이동 범위 밖 · ${this.terrainEffectText(terrain, unit, 'move')}`;
|
||||
this.showPointerFeedback(tile, 0xb64a45, `이동 불가: ${reason}`, key, 0xb64a45, 0.08, 0.75);
|
||||
}
|
||||
|
||||
@@ -4212,10 +4228,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
const preview = this.combatPreview(unit, target, action, usable);
|
||||
const locked = this.isLockedDamageTarget(unit, target, action, usable);
|
||||
const counter = preview.counterAvailable ? '반격 주의' : '반격 없음';
|
||||
const terrain = `${preview.terrainLabel} ${this.combatTerrainSummary(preview)}`;
|
||||
this.showPointerFeedback(
|
||||
tile,
|
||||
this.previewAccentColor(preview),
|
||||
`${target.name}: 피해 ${preview.damage} · ${this.previewSuccessLabel(preview)} ${preview.hitRate}% · ${counter}`,
|
||||
`${target.name}: 피해 ${preview.damage} · ${this.previewSuccessLabel(preview)} ${preview.hitRate}% · ${counter} · ${terrain}`,
|
||||
key,
|
||||
0xd9503f,
|
||||
0.16,
|
||||
@@ -4445,27 +4462,30 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private showMoveRange(unit: UnitData) {
|
||||
this.reachableTiles(unit).forEach(({ x, y }) => {
|
||||
this.reachableTiles(unit).forEach(({ x, y, cost }) => {
|
||||
const remaining = Math.max(0, unit.move - cost);
|
||||
const fillAlpha = Phaser.Math.Clamp(0.2 + remaining / Math.max(1, unit.move) * 0.18, 0.2, 0.38);
|
||||
const strokeAlpha = cost === 0 ? 0.94 : Phaser.Math.Clamp(0.48 + remaining / Math.max(1, unit.move) * 0.34, 0.48, 0.82);
|
||||
const marker = this.add.rectangle(
|
||||
this.tileTopLeftX(x),
|
||||
this.tileTopLeftY(y),
|
||||
this.layout.tileSize,
|
||||
this.layout.tileSize,
|
||||
palette.blue,
|
||||
0.34
|
||||
fillAlpha
|
||||
);
|
||||
marker.setData('tileX', x);
|
||||
marker.setData('tileY', y);
|
||||
marker.setOrigin(0);
|
||||
marker.setStrokeStyle(2, palette.blue, 0.84);
|
||||
marker.setStrokeStyle(cost === 0 ? 2 : 1, palette.blue, strokeAlpha);
|
||||
marker.setDepth(4);
|
||||
if (this.mapMask) {
|
||||
marker.setMask(this.mapMask);
|
||||
}
|
||||
marker.setVisible(this.isTileVisible(x, y));
|
||||
marker.setInteractive({ useHandCursor: true });
|
||||
marker.on('pointerover', () => marker.setFillStyle(palette.blue, 0.48));
|
||||
marker.on('pointerout', () => marker.setFillStyle(palette.blue, 0.34));
|
||||
marker.on('pointerover', () => marker.setFillStyle(palette.blue, Math.min(fillAlpha + 0.12, 0.5)));
|
||||
marker.on('pointerout', () => marker.setFillStyle(palette.blue, fillAlpha));
|
||||
marker.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
if (pointer.leftButtonDown()) {
|
||||
this.moveSelectedUnit(x, y, pointer);
|
||||
@@ -4605,6 +4625,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
return Array.from(reachable.values());
|
||||
}
|
||||
|
||||
private movementCostTo(unit: UnitData, x: number, y: number) {
|
||||
return this.reachableTiles(unit).find((tile) => tile.x === x && tile.y === y)?.cost;
|
||||
}
|
||||
|
||||
private movementCost(unit: UnitData, terrain: TerrainType) {
|
||||
const terrainRule = getTerrainRule(terrain);
|
||||
if (terrainRule.passable === false) {
|
||||
@@ -4614,6 +4638,97 @@ export class BattleScene extends Phaser.Scene {
|
||||
return unitClass.movementCosts?.[terrain] ?? terrainRule.moveCost;
|
||||
}
|
||||
|
||||
private terrainTone(terrain: TerrainType) {
|
||||
const terrainRule = getTerrainRule(terrain);
|
||||
if (terrainRule.passable === false) {
|
||||
return 0x8b6b64;
|
||||
}
|
||||
if (terrainRule.recoveryHp || terrainRule.moraleBonus) {
|
||||
return 0x59d18c;
|
||||
}
|
||||
if (terrainRule.defenseBonus >= 15) {
|
||||
return palette.gold;
|
||||
}
|
||||
if (terrain === 'road') {
|
||||
return 0xd8b15f;
|
||||
}
|
||||
if (terrain === 'forest' || terrain === 'hill') {
|
||||
return 0x82c486;
|
||||
}
|
||||
return 0x9fb0bf;
|
||||
}
|
||||
|
||||
private terrainEffectText(terrain: TerrainType, unit?: UnitData, mode: 'compact' | 'move' | 'detail' | 'card' = 'compact') {
|
||||
const terrainRule = getTerrainRule(terrain);
|
||||
const parts: string[] = [];
|
||||
if (terrainRule.passable === false) {
|
||||
parts.push('진입 불가');
|
||||
} else {
|
||||
const moveCost = unit ? this.movementCost(unit, terrain) : terrainRule.moveCost;
|
||||
parts.push(mode === 'move' ? `지형 이동 ${moveCost}` : mode === 'card' ? `이동${moveCost}` : `이동 ${moveCost}`);
|
||||
}
|
||||
|
||||
if (terrainRule.defenseBonus !== 0) {
|
||||
parts.push(`${mode === 'detail' ? '방어 ' : '방'}${this.formatSignedValue(terrainRule.defenseBonus)}`);
|
||||
}
|
||||
|
||||
if (unit) {
|
||||
const affinity = getUnitClass(unit.classKey).terrainRatings[terrain] ?? 100;
|
||||
if (affinity !== 100 || mode === 'detail') {
|
||||
parts.push(mode === 'card' ? `적성${affinity}%` : `적성 ${affinity}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'card' && terrainRule.recoveryHp && terrainRule.moraleBonus) {
|
||||
parts.push(
|
||||
terrainRule.recoveryHp === terrainRule.moraleBonus
|
||||
? `보급+${terrainRule.recoveryHp}`
|
||||
: `보급+${terrainRule.recoveryHp}/${terrainRule.moraleBonus}`
|
||||
);
|
||||
} else {
|
||||
if (terrainRule.recoveryHp) {
|
||||
parts.push(mode === 'card' ? `병력+${terrainRule.recoveryHp}` : `병력 +${terrainRule.recoveryHp}`);
|
||||
}
|
||||
if (terrainRule.moraleBonus) {
|
||||
parts.push(mode === 'card' ? `사기+${terrainRule.moraleBonus}` : `사기 +${terrainRule.moraleBonus}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' · ') || '기본 지형';
|
||||
}
|
||||
|
||||
private terrainTacticalNote(terrain: TerrainType) {
|
||||
switch (terrain) {
|
||||
case 'road':
|
||||
return '길은 방어 이득은 적지만 전열을 빠르게 재배치하기 좋습니다.';
|
||||
case 'forest':
|
||||
return '숲은 방어가 올라가지만 기병은 이동이 둔해집니다.';
|
||||
case 'hill':
|
||||
return '언덕은 궁병과 책사가 버티기 좋고 접근로를 좁힙니다.';
|
||||
case 'village':
|
||||
return '마을은 방어와 턴 시작 회복을 함께 주는 거점입니다.';
|
||||
case 'fort':
|
||||
return '요새는 피해를 크게 줄여 전열 고정에 좋습니다.';
|
||||
case 'camp':
|
||||
return '진영은 방어와 사기 보급을 동시에 기대할 수 있습니다.';
|
||||
case 'river':
|
||||
return '강은 건널 수 없습니다. 길목이나 얕은 우회로를 찾아야 합니다.';
|
||||
case 'cliff':
|
||||
return '절벽은 진입할 수 없는 차단 지형입니다.';
|
||||
default:
|
||||
return '평지는 이동과 전투 모두 기본 기준이 되는 지형입니다.';
|
||||
}
|
||||
}
|
||||
|
||||
private combatTerrainSummary(preview: Pick<CombatPreview, 'terrainDefenseBonus' | 'terrainHitPenalty' | 'terrainAffinity'>) {
|
||||
const parts = [
|
||||
preview.terrainDefenseBonus !== 0 ? `방${this.formatSignedValue(preview.terrainDefenseBonus)}` : '',
|
||||
preview.terrainHitPenalty > 0 ? `명-${preview.terrainHitPenalty}` : '',
|
||||
preview.terrainAffinity !== 100 ? `적성${preview.terrainAffinity}%` : ''
|
||||
].filter(Boolean);
|
||||
return parts.join(' / ') || '기본';
|
||||
}
|
||||
|
||||
private isInBounds(x: number, y: number) {
|
||||
return x >= 0 && y >= 0 && x < battleMap.width && y < battleMap.height;
|
||||
}
|
||||
@@ -5223,7 +5338,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
if (occupant) {
|
||||
this.deploymentNotice = `${occupant.name}이 있는 칸입니다. 시작 구역 안의 빈 칸을 선택하세요.`;
|
||||
this.deploymentNotice = `${occupant.name} 배치 칸입니다. 시작 구역 안의 빈 칸을 선택하세요.`;
|
||||
this.renderDeploymentPanel();
|
||||
return;
|
||||
}
|
||||
@@ -7444,8 +7559,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
const buff = this.battleBuffs.get(preview.attacker.id);
|
||||
const equipmentText = this.equipmentPreviewBonusText(preview);
|
||||
const terrainParts = [
|
||||
preview.terrainDefenseBonus > 0 ? `${preview.action === 'strategy' ? '저항' : '방'}+${preview.terrainDefenseBonus}` : '',
|
||||
preview.terrainHitPenalty > 0 ? `${preview.action === 'strategy' ? '성공' : '명'}-${preview.terrainHitPenalty}` : ''
|
||||
preview.terrainDefenseBonus !== 0
|
||||
? `${preview.action === 'strategy' ? '저항' : '방'}${this.formatSignedValue(preview.terrainDefenseBonus)}`
|
||||
: '',
|
||||
preview.terrainHitPenalty > 0 ? `${preview.action === 'strategy' ? '성공' : '명'}-${preview.terrainHitPenalty}` : '',
|
||||
preview.terrainAffinity !== 100 ? `적성${preview.terrainAffinity}%` : ''
|
||||
].filter(Boolean);
|
||||
|
||||
if (preview.action === 'strategy') {
|
||||
@@ -7677,7 +7795,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
badgeX = this.renderPreviewBadge(badgeX, badgeY, 'critical', `치명 ${preview.criticalRate}%`, maxBadgeRight);
|
||||
badgeX = this.renderPreviewBadge(badgeX, badgeY, 'advantage', preview.matchupLabel, maxBadgeRight);
|
||||
let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', preview.terrainLabel, maxBadgeRight);
|
||||
let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', `${preview.terrainLabel} ${this.combatTerrainSummary(preview)}`, maxBadgeRight);
|
||||
if (preview.bondDamageBonus > 0 && preview.bondLabel) {
|
||||
detailBadgeX = this.renderPreviewBadge(detailBadgeX, badgeY, 'leadership', `공명 +${preview.bondDamageBonus}%`, maxBadgeRight);
|
||||
}
|
||||
@@ -7859,22 +7977,22 @@ export class BattleScene extends Phaser.Scene {
|
||||
const card = this.trackSideObject(this.add.rectangle(cardX, y, cardWidth, 58, 0x0b1118, 0.96));
|
||||
card.setOrigin(0);
|
||||
card.setStrokeStyle(1, summary.tone, 0.62);
|
||||
const iconFrame = this.trackSideObject(this.add.rectangle(cardX + 21, y + 29, 38, 38, 0x16212d, 0.92));
|
||||
const iconFrame = this.trackSideObject(this.add.rectangle(cardX + 18, y + 29, 34, 34, 0x16212d, 0.92));
|
||||
iconFrame.setStrokeStyle(1, summary.tone, 0.5);
|
||||
this.trackSideIcon(cardX + 21, y + 29, summary.icon, 34);
|
||||
this.trackSideObject(this.add.text(cardX + 44, y + 7, summary.label, {
|
||||
this.trackSideIcon(cardX + 18, y + 29, summary.icon, 30);
|
||||
this.trackSideObject(this.add.text(cardX + 38, y + 7, summary.label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '10px',
|
||||
color: '#9fb0bf',
|
||||
fontStyle: '700',
|
||||
fixedWidth: cardWidth - 52
|
||||
fixedWidth: cardWidth - 44
|
||||
}));
|
||||
this.trackSideObject(this.add.text(cardX + 44, y + 24, this.truncateUiText(summary.value, 14), {
|
||||
this.trackSideObject(this.add.text(cardX + 38, y + 24, this.truncateUiText(summary.value, 14), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700',
|
||||
fixedWidth: cardWidth - 52
|
||||
fixedWidth: cardWidth - 44
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -9487,6 +9605,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
criticalRate,
|
||||
terrainDefenseBonus: terrainRule.defenseBonus,
|
||||
terrainHitPenalty,
|
||||
terrainAffinity: defenderTerrainRating,
|
||||
counterAvailable: this.canCounterAttack(defender, attacker, action),
|
||||
terrainLabel: terrainRule.label,
|
||||
matchupLabel: this.combatMatchupLabel(attacker, defender, action)
|
||||
@@ -15463,6 +15582,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private renderTerrainDetail(x: number, y: number) {
|
||||
this.clearSidePanelContent();
|
||||
this.setMiniMapVisible(true);
|
||||
const terrain = battleMap.terrain[y][x];
|
||||
const terrainRule = getTerrainRule(terrain);
|
||||
const { panelX, panelWidth } = this.layout;
|
||||
@@ -15471,34 +15591,53 @@ export class BattleScene extends Phaser.Scene {
|
||||
const top = this.sideContentTop();
|
||||
const attackDefense = Math.floor(terrainRule.defenseBonus / 3);
|
||||
const strategyDefense = Math.floor(terrainRule.defenseBonus / 4);
|
||||
const itemDefense = Math.floor(terrainRule.defenseBonus / 5);
|
||||
const hitPenalty = Math.max(0, Math.floor(terrainRule.defenseBonus / 2));
|
||||
const turnEffect = [
|
||||
terrainRule.recoveryHp ? `병력 +${terrainRule.recoveryHp}` : '',
|
||||
terrainRule.moraleBonus ? `사기 +${terrainRule.moraleBonus}` : ''
|
||||
].filter(Boolean).join(' / ') || '없음';
|
||||
|
||||
this.trackSideObject(this.add.text(left, top, '지형 정보', {
|
||||
const header = this.trackSideObject(this.add.rectangle(left, top, width, 58, 0x101820, 0.94));
|
||||
header.setOrigin(0);
|
||||
header.setStrokeStyle(1, this.terrainTone(terrain), 0.72);
|
||||
const iconFrame = this.trackSideObject(this.add.rectangle(left + 28, top + 29, 44, 44, 0x0a0f14, 0.92));
|
||||
iconFrame.setStrokeStyle(1, this.terrainTone(terrain), 0.58);
|
||||
this.trackSideIcon(left + 28, top + 29, 'terrain', 40);
|
||||
|
||||
this.trackSideObject(this.add.text(left + 58, top + 7, '지형 정보', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '24px',
|
||||
fontSize: '13px',
|
||||
color: '#9fb0bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
this.trackSideObject(this.add.text(left + 58, top + 27, terrainRule.label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '22px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
|
||||
this.renderSituationLine('좌표', `${x + 1}, ${y + 1}`, left, top + 48, width);
|
||||
this.renderSituationLine('지형', terrainDisplayLabels[terrain], left, top + 92, width);
|
||||
this.renderSituationLine('이동 비용', terrainRule.passable === false ? '진입 불가' : `${terrainRule.moveCost}`, left, top + 136, width);
|
||||
this.renderSituationLine('방어 보정', this.formatSignedValue(terrainRule.defenseBonus), left, top + 180, width);
|
||||
this.renderSituationLine('공격 방어', this.formatSignedValue(attackDefense), left, top + 224, width);
|
||||
this.renderSituationLine('책략 저항', this.formatSignedValue(strategyDefense), left, top + 268, width);
|
||||
this.renderSituationLine('도구 피해', this.formatSignedValue(itemDefense), left, top + 312, width);
|
||||
|
||||
const recovery =
|
||||
terrainRule.recoveryHp || terrainRule.moraleBonus
|
||||
? `\n턴 시작 시 병력 +${terrainRule.recoveryHp ?? 0}, 사기 +${terrainRule.moraleBonus ?? 0} 효과를 받습니다.`
|
||||
: '';
|
||||
const message =
|
||||
terrainRule.passable === false
|
||||
? '이 지형은 이동할 수 없습니다. 길목과 다리, 완만한 평지를 찾아 우회해야 합니다.'
|
||||
: terrainRule.defenseBonus > 0
|
||||
? '이 지형에 있는 부대는 방어 보정으로 받는 피해가 줄어듭니다.'
|
||||
: '이 지형은 기본 방어 보정이 없습니다.';
|
||||
this.renderPanelMessage(`${message}${recovery}`, left, top + 370, width, 76);
|
||||
this.renderSituationLine('좌표', `${x + 1}, ${y + 1}`, left, top + 76, width);
|
||||
this.renderSituationLine('이동', terrainRule.passable === false ? '진입 불가' : `기본 비용 ${terrainRule.moveCost}`, left, top + 120, width);
|
||||
this.renderSituationLine(
|
||||
'전투',
|
||||
terrainRule.defenseBonus === 0
|
||||
? '보정 없음'
|
||||
: `방어 ${this.formatSignedValue(terrainRule.defenseBonus)} / 명중 -${hitPenalty}`,
|
||||
left,
|
||||
top + 164,
|
||||
width
|
||||
);
|
||||
this.renderSituationLine(
|
||||
'피해 완화',
|
||||
terrainRule.defenseBonus === 0 ? '없음' : `공격 -${attackDefense} / 책략 -${strategyDefense}`,
|
||||
left,
|
||||
top + 208,
|
||||
width
|
||||
);
|
||||
this.renderSituationLine('턴 시작', turnEffect, left, top + 252, width);
|
||||
this.renderSituationLine('핵심 효과', this.terrainEffectText(terrain, undefined, 'compact'), left, top + 296, width);
|
||||
this.renderPanelMessage(this.terrainTacticalNote(terrain), left, top + 350, width, 64);
|
||||
}
|
||||
|
||||
private formatSignedValue(value: number) {
|
||||
@@ -15789,11 +15928,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
private unitEffectSummaries(unit: UnitData): UnitEffectSummary[] {
|
||||
const effects: UnitEffectSummary[] = [];
|
||||
const buff = this.battleBuffs.get(unit.id);
|
||||
const terrainRule = getTerrainRule(battleMap.terrain[unit.y][unit.x]);
|
||||
const turnStartParts = [
|
||||
terrainRule.recoveryHp ? `병력 +${terrainRule.recoveryHp}` : '',
|
||||
terrainRule.moraleBonus ? `사기 +${terrainRule.moraleBonus}` : ''
|
||||
].filter(Boolean);
|
||||
const terrain = battleMap.terrain[unit.y][unit.x];
|
||||
const terrainRule = getTerrainRule(terrain);
|
||||
const terrainValue = this.terrainEffectText(terrain, unit, 'card');
|
||||
|
||||
effects.push({
|
||||
icon: 'terrain',
|
||||
label: `${terrainRule.label} 지형`,
|
||||
value: terrainValue,
|
||||
tone: this.terrainTone(terrain),
|
||||
polarity: 'terrain'
|
||||
});
|
||||
|
||||
this.unitStatuses(unit).forEach((status) => {
|
||||
effects.push({
|
||||
@@ -15815,15 +15960,6 @@ export class BattleScene extends Phaser.Scene {
|
||||
polarity: 'buff'
|
||||
});
|
||||
}
|
||||
if (turnStartParts.length > 0) {
|
||||
effects.push({
|
||||
icon: 'terrain',
|
||||
label: `${terrainRule.label} 턴 시작`,
|
||||
value: turnStartParts.join(' / '),
|
||||
tone: 0x59d18c,
|
||||
polarity: 'terrain'
|
||||
});
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
@@ -15849,7 +15985,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
fontStyle: '700',
|
||||
fixedWidth: Math.max(36, cardWidth - 56)
|
||||
}));
|
||||
this.trackSideObject(this.add.text(cardX + 47, y + 25, this.truncateUiText(effect.value, 15), {
|
||||
this.trackSideObject(this.add.text(cardX + 47, y + 25, this.truncateUiText(effect.value, visible.length === 1 ? 30 : 15), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: '#f2e3bf',
|
||||
|
||||
Reference in New Issue
Block a user