Improve battle layout and visuals

This commit is contained in:
2026-06-22 00:35:48 +09:00
parent cc554ccf21
commit c232a80fcc
4 changed files with 281 additions and 49 deletions

View File

@@ -3,9 +3,6 @@ import { soundDirector } from '../audio/SoundDirector';
import { firstBattleMap, firstBattleUnits, type TerrainType, type UnitData } from '../data/scenario';
import { palette } from '../ui/palette';
const tileSize = 48;
const mapOffset = { x: 64, y: 72 };
const terrainColor: Record<TerrainType, number> = {
plain: 0x6d8056,
forest: 0x355f42,
@@ -13,7 +10,33 @@ const terrainColor: Record<TerrainType, number> = {
fort: 0x8b6f48
};
const unitTexture: Record<string, string> = {
'liu-bei': 'unit-liu-bei',
'guan-yu': 'unit-guan-yu',
'zhang-fei': 'unit-zhang-fei',
'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-cavalry',
'rebel-leader': 'unit-rebel-leader'
};
type BattleLayout = {
mapX: number;
mapY: number;
mapWidth: number;
mapHeight: number;
gridX: number;
gridY: number;
gridSize: number;
tileSize: number;
panelX: number;
panelY: number;
panelWidth: number;
panelHeight: number;
};
export class BattleScene extends Phaser.Scene {
private layout!: BattleLayout;
private selectedUnit?: UnitData;
private infoText?: Phaser.GameObjects.Text;
private turnText?: Phaser.GameObjects.Text;
@@ -25,105 +48,186 @@ export class BattleScene extends Phaser.Scene {
create() {
const { width, height } = this.scale;
this.layout = this.createLayout(width, height);
soundDirector.playMusic('battle-prep');
this.add.rectangle(0, 0, width, height, 0x0f1418).setOrigin(0);
this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
this.drawMap();
this.drawUnits();
this.drawSidePanel(width, height);
this.setInfo('아군 차례입니다. 유비를 선택해 이동 범위를 확인하세요.');
this.drawSidePanel();
this.setInfo('아군 차례입니다.\n유비를 선택해 이동 범위를 확인하세요.');
}
private createLayout(width: number, height: number): BattleLayout {
const margin = 24;
const gap = 20;
const panelWidth = Phaser.Math.Clamp(Math.floor(width * 0.27), 320, 360);
const mapWidth = width - panelWidth - gap - margin * 2;
const mapHeight = height - margin * 2;
const gridSize = Math.floor(Math.min(mapHeight - 36, mapWidth - 72) / firstBattleMap.width) * firstBattleMap.width;
const tileSize = gridSize / firstBattleMap.width;
const mapX = margin;
const mapY = margin;
return {
mapX,
mapY,
mapWidth,
mapHeight,
gridX: mapX + Math.floor((mapWidth - gridSize) / 2),
gridY: mapY + Math.floor((mapHeight - gridSize) / 2),
gridSize,
tileSize,
panelX: mapX + mapWidth + gap,
panelY: margin,
panelWidth,
panelHeight: mapHeight
};
}
private drawMap() {
const layout = this.layout;
const background = this.add.image(
layout.mapX + layout.mapWidth / 2,
layout.mapY + layout.mapHeight / 2,
'battle-map-first'
);
const source = this.textures.get('battle-map-first').getSourceImage();
const coverScale = Math.max(layout.mapWidth / source.width, layout.mapHeight / source.height);
background.setScale(coverScale);
background.setDepth(0);
const maskShape = this.add.graphics();
maskShape.fillStyle(0xffffff, 1);
maskShape.fillRect(layout.mapX, layout.mapY, layout.mapWidth, layout.mapHeight);
maskShape.setAlpha(0);
background.setMask(maskShape.createGeometryMask());
this.add.rectangle(layout.mapX, layout.mapY, layout.mapWidth, layout.mapHeight, 0x06090b, 0.16).setOrigin(0).setDepth(1);
this.add
.rectangle(layout.mapX, layout.mapY, layout.mapWidth, layout.mapHeight)
.setOrigin(0)
.setStrokeStyle(2, palette.gold, 0.62)
.setDepth(8);
firstBattleMap.terrain.forEach((row, y) => {
row.forEach((terrain, x) => {
const tileX = mapOffset.x + x * tileSize;
const tileY = mapOffset.y + y * tileSize;
const tile = this.add.rectangle(tileX, tileY, tileSize, tileSize, terrainColor[terrain]).setOrigin(0);
tile.setStrokeStyle(1, 0x182028, 0.8);
const tileX = layout.gridX + x * layout.tileSize;
const tileY = layout.gridY + y * layout.tileSize;
const terrainAlpha = terrain === 'plain' ? 0.03 : terrain === 'forest' ? 0.16 : terrain === 'hill' ? 0.13 : 0.18;
const tile = this.add.rectangle(tileX, tileY, layout.tileSize, layout.tileSize, terrainColor[terrain], terrainAlpha);
tile.setOrigin(0);
tile.setStrokeStyle(1, 0x0a1014, 0.5);
tile.setDepth(2);
});
});
this.add
.rectangle(layout.gridX, layout.gridY, layout.gridSize, layout.gridSize)
.setOrigin(0)
.setStrokeStyle(2, 0xf1d489, 0.48)
.setDepth(3);
}
private drawUnits() {
const layout = this.layout;
firstBattleUnits.forEach((unit) => {
const unitSize = unit.faction === 'ally' ? layout.tileSize * 0.78 : layout.tileSize * 0.72;
const sprite = this.add.image(
mapOffset.x + unit.x * tileSize + tileSize / 2,
mapOffset.y + unit.y * tileSize + tileSize / 2,
unit.faction === 'ally' ? 'unit-ally' : 'unit-enemy'
layout.gridX + unit.x * layout.tileSize + layout.tileSize / 2,
layout.gridY + unit.y * layout.tileSize + layout.tileSize / 2,
unitTexture[unit.id] ?? 'unit-rebel'
);
sprite.setDisplaySize(unitSize, unitSize);
sprite.setDepth(6);
sprite.setInteractive({ useHandCursor: unit.faction === 'ally' });
sprite.on('pointerdown', () => this.selectUnit(unit));
this.add.text(sprite.x - 22, sprite.y + 22, unit.name, {
const label = this.add.text(sprite.x, sprite.y + layout.tileSize * 0.35, unit.name, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: unit.faction === 'ally' ? '#e7edf7' : '#ffebe7',
backgroundColor: '#10131a',
padding: { left: 4, right: 4, top: 2, bottom: 2 }
backgroundColor: 'rgba(8,10,14,0.78)',
padding: { left: 5, right: 5, top: 2, bottom: 2 }
});
label.setOrigin(0.5, 0);
label.setDepth(7);
});
}
private drawSidePanel(width: number, height: number) {
const panelX = mapOffset.x + firstBattleMap.width * tileSize + 36;
this.add.rectangle(panelX, 72, width - panelX - 64, height - 128, palette.panel, 0.96).setOrigin(0);
this.add.text(panelX + 30, 108, '첫 전투', {
fontSize: '32px',
private drawSidePanel() {
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
this.add.rectangle(panelX, panelY, panelWidth, panelHeight, palette.panel, 0.96).setOrigin(0);
this.add.rectangle(panelX, panelY, 3, panelHeight, palette.gold, 0.82).setOrigin(0);
this.add.text(panelX + 24, panelY + 28, '첫 전투', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '30px',
color: '#e8dfca',
fontStyle: '700'
fontStyle: '700',
padding: { top: 4, bottom: 4 }
});
this.turnText = this.add.text(panelX + 30, 164, '1턴 / 아군', {
this.turnText = this.add.text(panelX + 24, panelY + 88, '1턴 / 아군', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '22px',
color: '#d8b15f'
});
this.infoText = this.add.text(panelX + 30, 224, '', {
this.infoText = this.add.text(panelX + 24, panelY + 144, '', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px',
color: '#e8dfca',
wordWrap: { width: width - panelX - 124, useAdvancedWrap: true },
wordWrap: { width: panelWidth - 48, useAdvancedWrap: true },
lineSpacing: 8
});
this.add.text(panelX + 30, height - 186, '승리 조건: 적 전멸', {
this.add.text(panelX + 24, panelY + panelHeight - 166, '승리 조건: 적 전멸', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '18px',
color: '#9aa3ad'
});
this.add.text(panelX + 30, height - 148, '패배 조건: 유비 퇴각', {
this.add.text(panelX + 24, panelY + panelHeight - 126, '패배 조건: 유비 퇴각', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '18px',
color: '#9aa3ad'
});
this.add.text(panelX + 30, height - 110, '다음 단계: 이동/공격/AI 구현', {
this.add.text(panelX + 24, panelY + panelHeight - 86, '다음 단계: 이동 / 공격 / AI', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '18px',
color: '#9aa3ad'
});
this.turnText.setText('1턴 / 아군');
}
private selectUnit(unit: UnitData) {
if (unit.faction !== 'ally') {
this.setInfo(`${unit.name}: ${unit.className}, HP ${unit.hp}/${unit.maxHp}`);
this.setInfo(`${unit.name}: ${unit.className}\nHP ${unit.hp}/${unit.maxHp}`);
return;
}
this.selectedUnit = unit;
this.clearMarkers();
this.showMoveRange(unit);
this.setInfo(`${unit.name} (${unit.className})\nHP ${unit.hp}/${unit.maxHp}, 공격 ${unit.attack}, 이동 ${unit.move}`);
this.setInfo(`${unit.name} (${unit.className})\nHP ${unit.hp}/${unit.maxHp}\n공격 ${unit.attack} / 이동 ${unit.move}`);
}
private showMoveRange(unit: UnitData) {
const layout = this.layout;
for (let y = 0; y < firstBattleMap.height; y += 1) {
for (let x = 0; x < firstBattleMap.width; x += 1) {
const distance = Math.abs(unit.x - x) + Math.abs(unit.y - y);
if (distance > 0 && distance <= unit.move) {
const marker = this.add.rectangle(
mapOffset.x + x * tileSize,
mapOffset.y + y * tileSize,
tileSize,
tileSize,
layout.gridX + x * layout.tileSize,
layout.gridY + y * layout.tileSize,
layout.tileSize,
layout.tileSize,
palette.blue,
0.24
);
marker.setOrigin(0);
marker.setStrokeStyle(1, palette.blue, 0.6);
marker.setStrokeStyle(1, palette.blue, 0.68);
marker.setDepth(4);
this.markers.push(marker);
}
}