Add turn menu and resonance bonds
This commit is contained in:
@@ -32,6 +32,15 @@ export type UnitStats = {
|
||||
luck: number;
|
||||
};
|
||||
|
||||
export type BattleBond = {
|
||||
id: string;
|
||||
unitIds: [string, string];
|
||||
title: string;
|
||||
level: number;
|
||||
exp: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type TerrainType = 'plain' | 'forest' | 'hill' | 'fort';
|
||||
|
||||
export type BattleMap = {
|
||||
@@ -239,3 +248,30 @@ export const firstBattleUnits: UnitData[] = [
|
||||
y: 4
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
unitIds: ['liu-bei', 'guan-yu'],
|
||||
title: '도원결의',
|
||||
level: 72,
|
||||
exp: 20,
|
||||
description: '같은 적을 노릴 때 공격 피해 보너스와 연계 공격 확률이 오른다.'
|
||||
},
|
||||
{
|
||||
id: 'liu-bei__zhang-fei',
|
||||
unitIds: ['liu-bei', 'zhang-fei'],
|
||||
title: '의형제',
|
||||
level: 68,
|
||||
exp: 12,
|
||||
description: '공격 목표가 겹치면 공명 경험치가 빠르게 쌓인다.'
|
||||
},
|
||||
{
|
||||
id: 'guan-yu__zhang-fei',
|
||||
unitIds: ['guan-yu', 'zhang-fei'],
|
||||
title: '맹장 공명',
|
||||
level: 64,
|
||||
exp: 8,
|
||||
description: '둘이 같은 전선을 압박하면 연계 공격의 기초 확률이 오른다.'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import Phaser from 'phaser';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
import { firstBattleMap, firstBattleUnits, type TerrainType, type UnitData, type UnitStats } from '../data/scenario';
|
||||
import {
|
||||
firstBattleBonds,
|
||||
firstBattleMap,
|
||||
firstBattleUnits,
|
||||
type BattleBond,
|
||||
type TerrainType,
|
||||
type UnitData,
|
||||
type UnitStats
|
||||
} from '../data/scenario';
|
||||
import { palette } from '../ui/palette';
|
||||
|
||||
const terrainColor: Record<TerrainType, number> = {
|
||||
@@ -43,6 +51,8 @@ type UnitView = {
|
||||
type BattlePhase = 'idle' | 'moving' | 'command';
|
||||
type BattleCommand = 'attack' | 'strategy' | 'item' | 'wait';
|
||||
type RosterTab = 'ally' | 'enemy';
|
||||
type ActiveFaction = 'ally' | 'enemy';
|
||||
type MapMenuAction = 'endTurn' | 'roster' | 'bond' | 'situation' | 'bgm' | 'close';
|
||||
|
||||
type CommandButton = {
|
||||
command: BattleCommand;
|
||||
@@ -58,6 +68,15 @@ type PendingMove = {
|
||||
toY: number;
|
||||
};
|
||||
|
||||
type AttackIntent = {
|
||||
attackerId: string;
|
||||
targetId: string;
|
||||
};
|
||||
|
||||
type BondState = BattleBond & {
|
||||
battleExp: number;
|
||||
};
|
||||
|
||||
const commandLabels: Record<BattleCommand, string> = {
|
||||
attack: '공격',
|
||||
strategy: '책략',
|
||||
@@ -70,6 +89,20 @@ const rosterLabels: Record<RosterTab, string> = {
|
||||
enemy: '적군'
|
||||
};
|
||||
|
||||
const factionLabels: Record<ActiveFaction, string> = {
|
||||
ally: '아군',
|
||||
enemy: '적군'
|
||||
};
|
||||
|
||||
const mapMenuLabels: Record<MapMenuAction, string> = {
|
||||
endTurn: '턴 종료',
|
||||
roster: '부대 일람',
|
||||
bond: '공명',
|
||||
situation: '전황',
|
||||
bgm: 'BGM',
|
||||
close: '닫기'
|
||||
};
|
||||
|
||||
const statLabels: Array<{ key: keyof UnitStats; label: string }> = [
|
||||
{ key: 'might', label: '무력' },
|
||||
{ key: 'intelligence', label: '지력' },
|
||||
@@ -81,6 +114,8 @@ const statLabels: Array<{ key: keyof UnitStats; label: string }> = [
|
||||
export class BattleScene extends Phaser.Scene {
|
||||
private layout!: BattleLayout;
|
||||
private phase: BattlePhase = 'idle';
|
||||
private turnNumber = 1;
|
||||
private activeFaction: ActiveFaction = 'ally';
|
||||
private selectedUnit?: UnitData;
|
||||
private turnText?: Phaser.GameObjects.Text;
|
||||
private markers: Phaser.GameObjects.Rectangle[] = [];
|
||||
@@ -88,8 +123,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
private sidePanelObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private commandButtons: CommandButton[] = [];
|
||||
private commandMenuObjects: Array<Phaser.GameObjects.Rectangle | Phaser.GameObjects.Text> = [];
|
||||
private mapMenuObjects: Array<Phaser.GameObjects.Rectangle | Phaser.GameObjects.Text> = [];
|
||||
private unitViews = new Map<string, UnitView>();
|
||||
private actedUnitIds = new Set<string>();
|
||||
private bondStates = new Map<string, BondState>();
|
||||
private attackIntents: AttackIntent[] = [];
|
||||
private pendingMove?: PendingMove;
|
||||
|
||||
constructor() {
|
||||
@@ -99,11 +137,12 @@ export class BattleScene extends Phaser.Scene {
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
this.layout = this.createLayout(width, height);
|
||||
this.bondStates = this.createBondStates();
|
||||
soundDirector.playMusic('battle-prep');
|
||||
this.input.mouse?.disableContextMenu();
|
||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
if (pointer.rightButtonDown()) {
|
||||
this.cancelPendingMove();
|
||||
this.handleRightClick(pointer);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,6 +150,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.drawMap();
|
||||
this.drawUnits();
|
||||
this.drawSidePanel();
|
||||
this.updateTurnText();
|
||||
this.renderRosterPanel('ally', '행동할 장수를 선택하세요.');
|
||||
}
|
||||
|
||||
@@ -247,6 +287,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private selectUnit(unit: UnitData) {
|
||||
this.hideMapMenu();
|
||||
|
||||
if (this.phase === 'command') {
|
||||
this.setInfo('먼저 현재 장수의 행동을 결정하세요.\n공격, 책략, 도구, 대기 중 하나를 선택해야 합니다.');
|
||||
return;
|
||||
@@ -262,6 +304,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeFaction !== 'ally') {
|
||||
this.renderUnitDetail(unit, '적군 행동 중에는 장수를 움직일 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.actedUnitIds.has(unit.id)) {
|
||||
this.selectedUnit = undefined;
|
||||
this.clearMarkers();
|
||||
@@ -482,12 +529,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
soundDirector.playSelect();
|
||||
|
||||
const actionMessage = this.commandResultMessage(unit, command);
|
||||
this.renderRosterPanel('ally', `${actionMessage}\n${this.remainingAllyCount()}명의 아군이 아직 행동할 수 있습니다.`);
|
||||
const remaining = this.remainingAllyCount();
|
||||
const turnHint = remaining > 0 ? `${remaining}명의 아군이 아직 행동할 수 있습니다.` : '모든 아군이 행동했습니다. 빈 맵 우클릭 메뉴에서 턴 종료를 선택하세요.';
|
||||
this.renderRosterPanel('ally', `${actionMessage}\n${turnHint}`);
|
||||
}
|
||||
|
||||
private commandResultMessage(unit: UnitData, command: BattleCommand) {
|
||||
if (command === 'attack') {
|
||||
return `${unit.name} 공격 명령 완료\n다음 구현에서 대상 선택과 피해 처리를 연결할 예정입니다.`;
|
||||
return this.recordAttackIntent(unit);
|
||||
}
|
||||
if (command === 'strategy') {
|
||||
return `${unit.name} 책략 명령 완료\n책략 목록과 MP는 다음 전투 확장 때 붙이겠습니다.`;
|
||||
@@ -502,9 +551,253 @@ export class BattleScene extends Phaser.Scene {
|
||||
return firstBattleUnits.filter((unit) => unit.faction === 'ally' && !this.actedUnitIds.has(unit.id)).length;
|
||||
}
|
||||
|
||||
private createBondStates() {
|
||||
return new Map(
|
||||
firstBattleBonds.map((bond) => [
|
||||
bond.id,
|
||||
{
|
||||
...bond,
|
||||
battleExp: 0
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
private handleRightClick(pointer: Phaser.Input.Pointer) {
|
||||
if (this.cancelPendingMove()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPointerOnEmptyMapTile(pointer)) {
|
||||
this.showMapMenu(pointer.x, pointer.y);
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideMapMenu();
|
||||
}
|
||||
|
||||
private isPointerOnEmptyMapTile(pointer: Phaser.Input.Pointer) {
|
||||
const tile = this.pointerToTile(pointer);
|
||||
if (!tile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.isOccupied(tile.x, tile.y);
|
||||
}
|
||||
|
||||
private pointerToTile(pointer: Phaser.Input.Pointer) {
|
||||
const { gridX, gridY, gridSize, tileSize } = this.layout;
|
||||
if (pointer.x < gridX || pointer.y < gridY || pointer.x >= gridX + gridSize || pointer.y >= gridY + gridSize) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
x: Math.floor((pointer.x - gridX) / tileSize),
|
||||
y: Math.floor((pointer.y - gridY) / tileSize)
|
||||
};
|
||||
}
|
||||
|
||||
private showMapMenu(pointerX: number, pointerY: number) {
|
||||
if (this.phase === 'command') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideMapMenu();
|
||||
this.clearMarkers();
|
||||
this.selectedUnit = undefined;
|
||||
this.pendingMove = undefined;
|
||||
this.phase = 'idle';
|
||||
|
||||
const actions: MapMenuAction[] = ['endTurn', 'roster', 'bond', 'situation', 'bgm', 'close'];
|
||||
const menuWidth = 148;
|
||||
const buttonHeight = 34;
|
||||
const padding = 8;
|
||||
const menuHeight = padding * 2 + actions.length * buttonHeight;
|
||||
const left = this.layout.mapX + 84;
|
||||
const top = Phaser.Math.Clamp(pointerY - menuHeight / 2, this.layout.mapY + 16, this.layout.mapY + this.layout.mapHeight - menuHeight - 16);
|
||||
|
||||
const panel = this.add.rectangle(left, top, menuWidth, menuHeight, 0x101821, 0.97);
|
||||
panel.setOrigin(0);
|
||||
panel.setDepth(20);
|
||||
panel.setStrokeStyle(2, palette.gold, 0.86);
|
||||
this.mapMenuObjects.push(panel);
|
||||
|
||||
actions.forEach((action, index) => {
|
||||
const y = top + padding + index * buttonHeight;
|
||||
const disabled = action === 'endTurn' && this.activeFaction !== 'ally';
|
||||
const bg = this.add.rectangle(left + 8, y, menuWidth - 16, buttonHeight - 4, disabled ? 0x121820 : 0x1a2630, disabled ? 0.72 : 0.94);
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(21);
|
||||
bg.setStrokeStyle(1, action === 'endTurn' ? 0xd8b15f : 0x5a7588, disabled ? 0.32 : 0.68);
|
||||
this.mapMenuObjects.push(bg);
|
||||
|
||||
const text = this.add.text(left + menuWidth / 2, y + buttonHeight / 2 - 2, mapMenuLabels[action], {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '17px',
|
||||
color: disabled ? '#737b84' : '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(22);
|
||||
this.mapMenuObjects.push(text);
|
||||
|
||||
if (!disabled) {
|
||||
const run = () => this.runMapMenuAction(action);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
|
||||
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.94));
|
||||
bg.on('pointerdown', run);
|
||||
text.setInteractive({ useHandCursor: true });
|
||||
text.on('pointerdown', run);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private runMapMenuAction(action: MapMenuAction) {
|
||||
this.hideMapMenu();
|
||||
soundDirector.playSelect();
|
||||
|
||||
if (action === 'endTurn') {
|
||||
this.endAllyTurn();
|
||||
return;
|
||||
}
|
||||
if (action === 'roster') {
|
||||
this.renderRosterPanel('ally', '부대 일람입니다.');
|
||||
return;
|
||||
}
|
||||
if (action === 'bond') {
|
||||
this.renderBondPanel();
|
||||
return;
|
||||
}
|
||||
if (action === 'situation') {
|
||||
this.renderSituationPanel();
|
||||
return;
|
||||
}
|
||||
if (action === 'bgm') {
|
||||
soundDirector.setMuted(!soundDirector.isMuted());
|
||||
this.renderSituationPanel(`BGM을 ${soundDirector.isMuted() ? '껐습니다' : '켰습니다'}.`);
|
||||
}
|
||||
}
|
||||
|
||||
private hideMapMenu() {
|
||||
this.mapMenuObjects.forEach((object) => object.destroy());
|
||||
this.mapMenuObjects = [];
|
||||
}
|
||||
|
||||
private endAllyTurn() {
|
||||
if (this.activeFaction !== 'ally') {
|
||||
this.renderSituationPanel('이미 적군 행동 중입니다.');
|
||||
return;
|
||||
}
|
||||
if (this.phase === 'command') {
|
||||
this.setInfo('현재 장수의 행동을 먼저 결정하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedUnit = undefined;
|
||||
this.pendingMove = undefined;
|
||||
this.phase = 'idle';
|
||||
this.clearMarkers();
|
||||
this.hideCommandMenu();
|
||||
|
||||
this.activeFaction = 'enemy';
|
||||
this.updateTurnText();
|
||||
this.renderSituationPanel('아군 턴을 종료했습니다.\n황건적은 아직 간단 AI 구현 전이라 정비만 하고 넘어갑니다.');
|
||||
|
||||
this.time.delayedCall(700, () => this.beginNextAllyTurn());
|
||||
}
|
||||
|
||||
private beginNextAllyTurn() {
|
||||
this.turnNumber += 1;
|
||||
this.activeFaction = 'ally';
|
||||
this.actedUnitIds.clear();
|
||||
this.attackIntents = [];
|
||||
this.resetActedStyles();
|
||||
this.updateTurnText();
|
||||
this.renderRosterPanel('ally', `${this.turnNumber}턴 아군 차례입니다.\n행동할 장수를 선택하세요.`);
|
||||
}
|
||||
|
||||
private updateTurnText() {
|
||||
this.turnText?.setText(`${this.turnNumber}턴 / ${factionLabels[this.activeFaction]}`);
|
||||
}
|
||||
|
||||
private resetActedStyles() {
|
||||
this.unitViews.forEach((view, unitId) => {
|
||||
const unit = firstBattleUnits.find((candidate) => candidate.id === unitId);
|
||||
view.sprite.postFX.clear();
|
||||
view.sprite.clearTint();
|
||||
view.sprite.setAlpha(1);
|
||||
view.label.setAlpha(1);
|
||||
view.label.setColor(unit?.faction === 'ally' ? '#e7edf7' : '#ffebe7');
|
||||
view.label.setBackgroundColor('rgba(8,10,14,0.78)');
|
||||
});
|
||||
}
|
||||
|
||||
private recordAttackIntent(unit: UnitData) {
|
||||
const target = this.findNearestEnemy(unit);
|
||||
if (!target) {
|
||||
return `${unit.name} 공격 명령 완료\n공격 가능한 적이 없습니다.`;
|
||||
}
|
||||
|
||||
const partners = this.attackIntents
|
||||
.filter((intent) => intent.targetId === target.id && intent.attackerId !== unit.id)
|
||||
.map((intent) => intent.attackerId);
|
||||
let gained = 0;
|
||||
|
||||
partners.forEach((partnerId) => {
|
||||
const bond = this.findBond(unit.id, partnerId);
|
||||
if (!bond) {
|
||||
return;
|
||||
}
|
||||
bond.battleExp += 4;
|
||||
bond.exp += 4;
|
||||
while (bond.exp >= 100) {
|
||||
bond.exp -= 100;
|
||||
bond.level = Math.min(100, bond.level + 1);
|
||||
}
|
||||
gained += 4;
|
||||
});
|
||||
|
||||
this.attackIntents = this.attackIntents.filter((intent) => intent.attackerId !== unit.id);
|
||||
this.attackIntents.push({ attackerId: unit.id, targetId: target.id });
|
||||
|
||||
const bonus = this.strongestBondBonus(unit.id);
|
||||
const chain = bonus.chainRate > 0 ? `\n공명 보너스: 피해 +${bonus.damageBonus}% / 연계 ${bonus.chainRate}%` : '';
|
||||
const exp = gained > 0 ? `\n같은 목표를 노린 공명 경험치 +${gained}` : '\n같은 목표를 노린 장수가 생기면 공명 경험치가 쌓입니다.';
|
||||
return `${unit.name} 공격 명령 완료\n예상 목표: ${target.name}${chain}${exp}`;
|
||||
}
|
||||
|
||||
private findNearestEnemy(unit: UnitData) {
|
||||
const enemies = firstBattleUnits.filter((candidate) => candidate.faction === 'enemy' && candidate.hp > 0);
|
||||
return enemies.sort((a, b) => this.tileDistance(unit, a) - this.tileDistance(unit, b))[0];
|
||||
}
|
||||
|
||||
private tileDistance(a: UnitData, b: UnitData) {
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
}
|
||||
|
||||
private findBond(unitId: string, partnerId: string) {
|
||||
return Array.from(this.bondStates.values()).find((bond) => {
|
||||
return bond.unitIds.includes(unitId) && bond.unitIds.includes(partnerId);
|
||||
});
|
||||
}
|
||||
|
||||
private strongestBondBonus(unitId: string) {
|
||||
const related = Array.from(this.bondStates.values()).filter((bond) => bond.unitIds.includes(unitId));
|
||||
const strongest = related.sort((a, b) => b.level - a.level)[0];
|
||||
if (!strongest) {
|
||||
return { damageBonus: 0, chainRate: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
damageBonus: Math.floor(strongest.level / 20) * 3,
|
||||
chainRate: strongest.level >= 70 ? 18 : strongest.level >= 50 ? 8 : 0
|
||||
};
|
||||
}
|
||||
|
||||
private cancelPendingMove() {
|
||||
if (this.phase !== 'command' || !this.pendingMove) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unit, fromX, fromY, toX, toY } = this.pendingMove;
|
||||
@@ -524,6 +817,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
unit,
|
||||
`이동을 취소했습니다.\n${toX + 1}, ${toY + 1}에서 원래 위치 ${fromX + 1}, ${fromY + 1}로 되돌렸습니다.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private moveUnitView(
|
||||
@@ -570,6 +864,126 @@ export class BattleScene extends Phaser.Scene {
|
||||
view.label.setBackgroundColor('rgba(20,20,20,0.66)');
|
||||
}
|
||||
|
||||
private renderBondPanel(message?: string) {
|
||||
this.clearSidePanelContent();
|
||||
const { panelX, panelY, panelWidth } = this.layout;
|
||||
const left = panelX + 24;
|
||||
const width = panelWidth - 48;
|
||||
const top = panelY + 132;
|
||||
|
||||
this.trackSideObject(this.add.text(left, top, '공명 관계', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '22px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
this.trackSideObject(this.add.text(left, top + 34, '같은 적을 노리거나 전투 사이 이벤트로 상승합니다.', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#9fb0bf'
|
||||
}));
|
||||
|
||||
Array.from(this.bondStates.values()).forEach((bond, index) => {
|
||||
const y = top + 70 + index * 104;
|
||||
const [first, second] = bond.unitIds.map((unitId) => this.unitName(unitId));
|
||||
const bg = this.trackSideObject(this.add.rectangle(left, y, width, 92, 0x101820, 0.9));
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(1, bond.level >= 70 ? palette.gold : palette.blue, 0.66);
|
||||
|
||||
this.trackSideObject(this.add.text(left + 12, y + 8, `${first} · ${second}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '17px',
|
||||
color: '#e8dfca',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
const levelText = this.trackSideObject(this.add.text(left + width - 12, y + 8, `${bond.level}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
levelText.setOrigin(1, 0);
|
||||
|
||||
this.trackSideObject(this.add.text(left + 12, y + 33, bond.title, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#d8b15f'
|
||||
}));
|
||||
this.drawGauge(left + 86, y + 39, width - 148, 8, bond.exp / 100, bond.level >= 70 ? 0xd8b15f : 0x58aee0);
|
||||
|
||||
const bonus = this.bondBonus(bond);
|
||||
this.trackSideObject(this.add.text(left + 12, y + 59, `피해 +${bonus.damageBonus}% 연계 ${bonus.chainRate}% 전투 +${bond.battleExp}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#d4dce6'
|
||||
}));
|
||||
});
|
||||
|
||||
this.renderPanelMessage(message ?? '공격 명령에서 같은 예상 목표를 노리면 공명 경험치가 쌓입니다.', left, top + 396, width);
|
||||
}
|
||||
|
||||
private renderSituationPanel(message?: string) {
|
||||
this.clearSidePanelContent();
|
||||
const { panelX, panelY, panelWidth } = this.layout;
|
||||
const left = panelX + 24;
|
||||
const width = panelWidth - 48;
|
||||
const top = panelY + 132;
|
||||
const actedCount = firstBattleUnits.filter((unit) => unit.faction === 'ally' && this.actedUnitIds.has(unit.id)).length;
|
||||
const allyCount = firstBattleUnits.filter((unit) => unit.faction === 'ally').length;
|
||||
const enemyCount = firstBattleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp > 0).length;
|
||||
const bestBond = Array.from(this.bondStates.values()).sort((a, b) => b.level - a.level)[0];
|
||||
|
||||
this.trackSideObject(this.add.text(left, top, '전황', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '24px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
|
||||
this.renderSituationLine('현재 턴', `${this.turnNumber}턴 / ${factionLabels[this.activeFaction]}`, left, top + 48, width);
|
||||
this.renderSituationLine('아군 행동', `${actedCount} / ${allyCount}`, left, top + 92, width);
|
||||
this.renderSituationLine('남은 적', `${enemyCount}`, left, top + 136, width);
|
||||
this.renderSituationLine('BGM', soundDirector.isMuted() ? '꺼짐' : '켜짐', left, top + 180, width);
|
||||
this.renderSituationLine(
|
||||
'최고 공명',
|
||||
bestBond ? `${this.unitName(bestBond.unitIds[0])}·${this.unitName(bestBond.unitIds[1])} ${bestBond.level}` : '-',
|
||||
left,
|
||||
top + 224,
|
||||
width
|
||||
);
|
||||
|
||||
this.renderPanelMessage(message ?? '빈 맵에서 우클릭하면 전술 메뉴를 다시 열 수 있습니다.', left, top + 286, width);
|
||||
}
|
||||
|
||||
private renderSituationLine(label: string, value: string, x: number, y: number, width: number) {
|
||||
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 34, 0x101820, 0.86));
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(1, 0x53606c, 0.46);
|
||||
this.trackSideObject(this.add.text(x + 12, y + 8, label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '15px',
|
||||
color: '#9fb0bf'
|
||||
}));
|
||||
const valueText = this.trackSideObject(this.add.text(x + width - 12, y + 7, value, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '16px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
valueText.setOrigin(1, 0);
|
||||
}
|
||||
|
||||
private bondBonus(bond: BondState) {
|
||||
return {
|
||||
damageBonus: Math.floor(bond.level / 20) * 3,
|
||||
chainRate: bond.level >= 70 ? 18 : bond.level >= 50 ? 8 : 0
|
||||
};
|
||||
}
|
||||
|
||||
private unitName(unitId: string) {
|
||||
return firstBattleUnits.find((unit) => unit.id === unitId)?.name ?? unitId;
|
||||
}
|
||||
|
||||
private renderRosterPanel(tab: RosterTab = this.rosterTab, message?: string) {
|
||||
this.rosterTab = tab;
|
||||
this.clearSidePanelContent();
|
||||
|
||||
Reference in New Issue
Block a user